mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 04:36:21 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba38b8bf27 | ||
|
|
38513735dd | ||
|
|
acfc7d605b | ||
|
|
df42538319 | ||
|
|
b02259e926 | ||
|
|
2b6bd9fb51 | ||
|
|
aadedbb610 | ||
|
|
d15a324137 | ||
|
|
119fac17bf | ||
|
|
088881b674 | ||
|
|
f008d5367b | ||
|
|
1ace185a57 | ||
|
|
a01d26e25d | ||
|
|
7e2ef9aacc | ||
|
|
4a98d064f7 | ||
|
|
b0d6cea52a | ||
|
|
d6bf9ed101 | ||
|
|
7e940e515b | ||
|
|
29e1ff0754 | ||
|
|
6309100cc4 | ||
|
|
ec3052c823 | ||
|
|
a28b800228 | ||
|
|
713a64ab45 | ||
|
|
4d5ffe38fc | ||
|
|
85460157b1 | ||
|
|
35e5b7620e | ||
|
|
e4c06b14a7 | ||
|
|
7fe3258269 | ||
|
|
ec19b4d4fb | ||
|
|
9301ead060 | ||
|
|
bc29ece526 | ||
|
|
4158c7a363 | ||
|
|
944d1059da | ||
|
|
bf18d8e106 | ||
|
|
bf216e0917 | ||
|
|
2405b6dbbf | ||
|
|
e64f2d195e | ||
|
|
f66bec8caf | ||
|
|
3eb79e3adb | ||
|
|
07e2ac3ca0 |
@@ -14,11 +14,6 @@ On Linux the plugin is available via the **Flatpak** package manager for users w
|
||||
flatpak install com.obsproject.Studio.Plugin.SceneSwitcher
|
||||
```
|
||||
|
||||
The **Snap** package manager offers an OBS Studio installation which is bundled with the plugin:
|
||||
```
|
||||
sudo snap install obs-studio
|
||||
```
|
||||
|
||||
More information can be found [here](https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -145,11 +145,7 @@ AdvSceneSwitcher.condition.file="Datei"
|
||||
AdvSceneSwitcher.condition.file.type.match="entspricht"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
|
||||
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
|
||||
AdvSceneSwitcher.condition.file.local="Lokale Datei"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.media="Medien"
|
||||
AdvSceneSwitcher.condition.media.source="Quelle"
|
||||
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
|
||||
@@ -412,7 +408,6 @@ AdvSceneSwitcher.action.recording.type.unpause="Aufnahme nicht mehr pausieren"
|
||||
AdvSceneSwitcher.action.recording.type.split="Aufnahme-Datei teilen"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Bitte beachten, dass je nach Aufnahmeeinstellung die Aufnahme möglicherweise nicht unterbrochen werden kann"
|
||||
AdvSceneSwitcher.action.recording.split.hint="Vergewissern, dass die automatische Dateiaufteilung in den OBS-Einstellungen aktiviert ist!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Replay Buffer"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Warnung: Ein zu häufiges Speichern kann dazu führen, dass der Replay Buffer nicht gespeichert wird!"
|
||||
AdvSceneSwitcher.action.replay.type.stop="Replay Buffer stoppen"
|
||||
@@ -505,9 +500,9 @@ AdvSceneSwitcher.action.transition.type.scene="Szenenübergang"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
|
||||
AdvSceneSwitcher.action.transition.layout.type="Anpassen {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
|
||||
AdvSceneSwitcher.action.timer="Timer"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"
|
||||
|
||||
@@ -40,6 +40,7 @@ AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Show system t
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailure="Display warning if plugins cannot be loaded"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog="Do not ask to keep the plugin stopped after an unclean shutdown"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage="<html><body>Loading of the following plugin libraries was unsuccessful, which could result in some Advanced Scene Switcher functions not being available:%1Check the OBS logs for details.<br>This message can be disabled on the General tab.</body></html>"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.warnCorruptedInstallMessage="The plugin installation seems to be corrupted and might crash!\nPlease make sure the plugin was installed correctly!"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can be represented via macros"
|
||||
@@ -368,11 +369,7 @@ AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
|
||||
AdvSceneSwitcher.condition.file.type.exists="exists"
|
||||
AdvSceneSwitcher.condition.file.type.isFile="is a file"
|
||||
AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
|
||||
AdvSceneSwitcher.condition.file.remote="Remote file"
|
||||
AdvSceneSwitcher.condition.file.local="Local file"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.media="Media"
|
||||
AdvSceneSwitcher.condition.media.checkType.state="State matches"
|
||||
AdvSceneSwitcher.condition.media.checkType.time="Time restriction matches"
|
||||
@@ -676,6 +673,34 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
|
||||
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
|
||||
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
|
||||
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
|
||||
AdvSceneSwitcher.condition.http="HTTP"
|
||||
AdvSceneSwitcher.condition.http.layout="Receive{{method}}request on{{server}}"
|
||||
AdvSceneSwitcher.condition.http.layout.path="Path:{{path}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.http.layout.body="Body:{{body}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.http.method.any="any"
|
||||
AdvSceneSwitcher.condition.http.method.get="GET"
|
||||
AdvSceneSwitcher.condition.http.method.post="POST"
|
||||
AdvSceneSwitcher.condition.http.method.put="PUT"
|
||||
AdvSceneSwitcher.condition.http.method.patch="PATCH"
|
||||
AdvSceneSwitcher.condition.http.method.delete="DELETE"
|
||||
AdvSceneSwitcher.httpServer.select="Select HTTP server"
|
||||
AdvSceneSwitcher.httpServer.add="Add HTTP server"
|
||||
AdvSceneSwitcher.httpServer.configure="Configure HTTP server"
|
||||
AdvSceneSwitcher.httpServer.invalid="Invalid HTTP server"
|
||||
AdvSceneSwitcher.httpServer.name="Name:"
|
||||
AdvSceneSwitcher.httpServer.port="Port:"
|
||||
AdvSceneSwitcher.httpServer.startOnLoad="Start on load"
|
||||
AdvSceneSwitcher.httpServer.status.listening="Listening"
|
||||
AdvSceneSwitcher.httpServer.status.stopped="Stopped"
|
||||
AdvSceneSwitcher.httpServerTab.title="HTTP Servers"
|
||||
AdvSceneSwitcher.httpServerTab.help="No HTTP servers configured.\nAdd one to receive incoming HTTP requests in conditions."
|
||||
AdvSceneSwitcher.httpServerTab.addButton.tooltip="Add HTTP server"
|
||||
AdvSceneSwitcher.httpServerTab.removeButton.tooltip="Remove HTTP server"
|
||||
AdvSceneSwitcher.httpServerTab.name.header="Name"
|
||||
AdvSceneSwitcher.httpServerTab.port.header="Port"
|
||||
AdvSceneSwitcher.httpServerTab.status.header="Status"
|
||||
AdvSceneSwitcher.httpServerTab.removeSingle.text="Are you sure you want to remove \"%1\"?"
|
||||
AdvSceneSwitcher.httpServerTab.removeMultiple.text="Are you sure you want to remove %1 HTTP servers?"
|
||||
AdvSceneSwitcher.condition.temporaryVariable="Macro property"
|
||||
AdvSceneSwitcher.condition.variable="Variable"
|
||||
AdvSceneSwitcher.condition.variable.type.compare="equals"
|
||||
@@ -879,6 +904,15 @@ AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Fade{{fadeTypes}}{{rate}}per se
|
||||
AdvSceneSwitcher.action.audio.fade.wait="Wait for fade to complete."
|
||||
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
|
||||
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}{{track}}"
|
||||
AdvSceneSwitcher.action.playAudio="Play Audio File"
|
||||
AdvSceneSwitcher.action.playAudio.file.browse="Browse for audio file"
|
||||
AdvSceneSwitcher.action.playAudio.layout.volume="Volume{{volumeDB}}"
|
||||
AdvSceneSwitcher.action.playAudio.layout.monitor="Monitoring{{monitorTypes}}"
|
||||
AdvSceneSwitcher.action.playAudio.wait="Wait for playback to complete"
|
||||
AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not available"
|
||||
AdvSceneSwitcher.action.playAudio.tracks="Tracks"
|
||||
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
|
||||
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
|
||||
AdvSceneSwitcher.action.recording="Recording"
|
||||
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
|
||||
AdvSceneSwitcher.action.recording.type.start="Start recording"
|
||||
@@ -887,9 +921,10 @@ AdvSceneSwitcher.action.recording.type.unpause="Unpause recording"
|
||||
AdvSceneSwitcher.action.recording.type.split="Split recording file"
|
||||
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
|
||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename formatting"
|
||||
AdvSceneSwitcher.action.recording.type.addChapter="Add chapter"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording"
|
||||
AdvSceneSwitcher.action.recording.split.hint="Make sure to enable automatic file splitting in the OBS settings first!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{chapterName}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Replay buffer"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!"
|
||||
AdvSceneSwitcher.action.replay.durationWarn="Warning: Changing the maximum replay time will only apply the next time the replay buffer is started!"
|
||||
@@ -949,6 +984,8 @@ AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog
|
||||
AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog"
|
||||
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
|
||||
AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties dialog"
|
||||
AdvSceneSwitcher.action.source.type.getSetting="Get setting value"
|
||||
AdvSceneSwitcher.action.source.type.getSettings="Get settings JSON"
|
||||
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}"
|
||||
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
|
||||
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
|
||||
@@ -971,6 +1008,49 @@ AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
|
||||
AdvSceneSwitcher.action.source.refresh="Refresh"
|
||||
AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value."
|
||||
AdvSceneSwitcher.action.source.dialog.accept="Accept changes"
|
||||
AdvSceneSwitcher.action.sourceInteraction="Source Interaction"
|
||||
AdvSceneSwitcher.action.sourceInteraction.source="Source"
|
||||
AdvSceneSwitcher.action.sourceInteraction.noSelection="Select a step to edit it"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record="Record interaction ..."
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove="Mouse move (%1, %2)"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp="up"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown="down"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick="Mouse %1 %2 (%3, %4)"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount=" x%1"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel="Mouse wheel (%1, %2) dx=%3 dy=%4"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp="up"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown="down"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress="Key %1"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText="Key %1 '%2'"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText="Type \"%1\""
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait="Wait %1 ms"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.type="Type"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove="Mouse move"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick="Mouse click"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel="Mouse wheel"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress="Key press"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText="Type text"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.wait="Wait"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.x="X"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.y="Y"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.button="Button"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp="Mouse up"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount="Click count"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx="Delta X"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy="Delta Y"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey="Virtual key"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp="Key up"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.text="Text"
|
||||
AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs="Duration (ms)"
|
||||
AdvSceneSwitcher.action.sourceInteraction.button.left="Left"
|
||||
AdvSceneSwitcher.action.sourceInteraction.button.middle="Middle"
|
||||
AdvSceneSwitcher.action.sourceInteraction.button.right="Right"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.title="Record Source Interactions"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.start="Start recording"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.stop="Stop recording"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.accept="Accept"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.placeholder="Start recording and interact with the source to capture steps"
|
||||
AdvSceneSwitcher.action.sourceInteraction.record.invalidSource="No valid source is selected.\nPlease select a source that supports interaction before recording."
|
||||
AdvSceneSwitcher.action.media="Media"
|
||||
AdvSceneSwitcher.action.media.type.play="Play"
|
||||
AdvSceneSwitcher.action.media.type.pause="Pause"
|
||||
@@ -1005,6 +1085,7 @@ AdvSceneSwitcher.action.macro.type.stop="Stop actions"
|
||||
AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
|
||||
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
|
||||
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
|
||||
AdvSceneSwitcher.action.macro.type.getInfo="Get macro info"
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
|
||||
@@ -1087,13 +1168,16 @@ AdvSceneSwitcher.action.studioMode.type.enable="Enable studio mode"
|
||||
AdvSceneSwitcher.action.studioMode.type.disable="Disable studio mode"
|
||||
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
|
||||
AdvSceneSwitcher.action.transition="Transition"
|
||||
AdvSceneSwitcher.action.transition.type.scene="scene transition"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds"
|
||||
AdvSceneSwitcher.action.transition.type.scene="Modify scene transition"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="Modify scene transition override"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="Modify source show transition"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="Modify source hide transition"
|
||||
AdvSceneSwitcher.action.transition.type.tbar="Modify T-Bar position"
|
||||
AdvSceneSwitcher.action.transition.type.releaseTbar="Release T-Bar"
|
||||
AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Set transition type to{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Set transition duration to{{duration}}seconds"
|
||||
AdvSceneSwitcher.action.transition.layout.tbar="Set T-Bar position to{{tbarPosition}}"
|
||||
AdvSceneSwitcher.action.timer="Timer"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Pause"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Continue"
|
||||
@@ -1253,23 +1337,52 @@ AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Set stream catego
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.tags.set="Set stream tags"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language"
|
||||
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
|
||||
AdvSceneSwitcher.action.twitch.type.raid.end="Cancel raid"
|
||||
AdvSceneSwitcher.action.twitch.type.shoutout.send="Send shoutout"
|
||||
AdvSceneSwitcher.action.twitch.type.shieldMode.start="Enable shield mode"
|
||||
AdvSceneSwitcher.action.twitch.type.shieldMode.end="Disable shield mode"
|
||||
AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial"
|
||||
AdvSceneSwitcher.action.twitch.type.commercial.snooze="Snooze next ad"
|
||||
AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker"
|
||||
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.enable="Enable chat's follower-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.disable="Disable chat's follower-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.enable="Enable chat's subscriber-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.disable="Disable chat's subscriber-only mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.slowMode.enable="Enable chat's slow mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.slowMode.disable="Disable chat's slow mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.enable="Enable chat's non-moderator message delay"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.disable="Disable chat's non-moderator message delay"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.enable="Enable chat's unique message mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.disable="Disable chat's unique message mode"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Send chat message"
|
||||
AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information"
|
||||
AdvSceneSwitcher.action.twitch.type.user.ban="Ban user"
|
||||
AdvSceneSwitcher.action.twitch.type.user.unban="Unban user"
|
||||
AdvSceneSwitcher.action.twitch.type.user.block="Block user"
|
||||
AdvSceneSwitcher.action.twitch.type.user.unblock="Unblock user"
|
||||
AdvSceneSwitcher.action.twitch.type.user.moderator.add="Add user as moderator"
|
||||
AdvSceneSwitcher.action.twitch.type.user.moderator.delete="Remove user as moderator"
|
||||
AdvSceneSwitcher.action.twitch.type.user.vip.add="Add user as VIP"
|
||||
AdvSceneSwitcher.action.twitch.type.user.vip.delete="Remove user as VIP"
|
||||
AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
|
||||
AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control"
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{nonModDelayDuration}}{{channel}}{{pointsReward}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.chat="Using account{{account}}{{actions}}on{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="Using account{{account}}{{actions}}of{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo="Using account{{account}}{{actions}}for{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="Using account{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="for{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="Using account{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="Using account{{account}}{{actions}}timeout{{duration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}reason{{banReason}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="Using account{{account}}{{actions}}for channel{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.title.title="Enter title"
|
||||
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
|
||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
|
||||
@@ -1290,6 +1403,8 @@ AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the l
|
||||
AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached"
|
||||
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.enable="Enable branded content"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.disable="Disable branded content"
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences."
|
||||
@@ -1395,6 +1510,7 @@ AdvSceneSwitcher.askBackup="Detected a new version of the Advanced Scene Switche
|
||||
AdvSceneSwitcher.askForMacro="Select macro{{macroSelection}}"
|
||||
|
||||
AdvSceneSwitcher.crashDetected="OBS did not shut down cleanly (for example, due to a crash or freeze).\n\nThe Advanced Scene Switcher plugin would normally start automatically.\nWould you like to keep it stopped for now?"
|
||||
AdvSceneSwitcher.crashDetected.suppressCheckbox="Do not show this dialog again"
|
||||
|
||||
AdvSceneSwitcher.close="Close"
|
||||
AdvSceneSwitcher.browse="Browse"
|
||||
@@ -1664,6 +1780,7 @@ AdvSceneSwitcher.script.language.lua="LUA"
|
||||
AdvSceneSwitcher.script.language.select="--select language--"
|
||||
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
|
||||
AdvSceneSwitcher.script.file.open="Open"
|
||||
AdvSceneSwitcher.script.file.select="Select Script File"
|
||||
AdvSceneSwitcher.script.file.open.failed="Could not open script file!"
|
||||
AdvSceneSwitcher.script.file.warning.notFound="Warning: The given file path was not found!"
|
||||
AdvSceneSwitcher.script.file.warning.openFail="Warning: Could not open the script file!"
|
||||
@@ -2181,6 +2298,18 @@ AdvSceneSwitcher.tempVar.macro.runCount="Run count"
|
||||
AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed."
|
||||
AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count"
|
||||
AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of which the condition state was true."
|
||||
AdvSceneSwitcher.tempVar.macro.info.conditionCount="Condition count"
|
||||
AdvSceneSwitcher.tempVar.macro.info.conditionCount.description="The number of conditions in the macro."
|
||||
AdvSceneSwitcher.tempVar.macro.info.actionCount="Action count"
|
||||
AdvSceneSwitcher.tempVar.macro.info.actionCount.description="The number of actions in the macro."
|
||||
AdvSceneSwitcher.tempVar.macro.info.elseActionCount="Else-action count"
|
||||
AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description="The number of else-actions in the macro."
|
||||
AdvSceneSwitcher.tempVar.macro.info.paused="Paused"
|
||||
AdvSceneSwitcher.tempVar.macro.info.paused.description="Whether the macro is currently paused. Value is \"true\" or \"false\"."
|
||||
AdvSceneSwitcher.tempVar.macro.info.runCount="Run count"
|
||||
AdvSceneSwitcher.tempVar.macro.info.runCount.description="The number of times the macro has been executed."
|
||||
AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun="Seconds since last run"
|
||||
AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description="The number of seconds elapsed since the macro was last executed. Value is -1 if the macro has never been executed."
|
||||
|
||||
AdvSceneSwitcher.tempVar.process.name="Process name"
|
||||
|
||||
@@ -2213,6 +2342,8 @@ AdvSceneSwitcher.tempVar.video.text="OCR text"
|
||||
AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame."
|
||||
AdvSceneSwitcher.tempVar.video.color="Average color"
|
||||
AdvSceneSwitcher.tempVar.video.color.description="The average RGB color in a given video input frame in HexArgb format."
|
||||
AdvSceneSwitcher.tempVar.video.dominantColor="Dominant color"
|
||||
AdvSceneSwitcher.tempVar.video.dominantColor.description="The most dominant RGB color in a given video input frame in HexArgb format.\nDetermined using k-means clustering, which groups similar colors and returns the center of the largest group.\nDue to the nature of the algorithm the result may vary slightly between evaluations."
|
||||
|
||||
AdvSceneSwitcher.tempVar.websocket.message="Received websocket message"
|
||||
AdvSceneSwitcher.tempVar.websocket.message.description="The received websocket message, which matched the given pattern"
|
||||
@@ -2342,9 +2473,14 @@ AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
|
||||
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
|
||||
|
||||
AdvSceneSwitcher.tempVar.http.status="Status code"
|
||||
AdvSceneSwitcher.tempVar.http.body="Message body"
|
||||
AdvSceneSwitcher.tempVar.http.error="Error"
|
||||
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
|
||||
AdvSceneSwitcher.tempVar.http.method="Method"
|
||||
AdvSceneSwitcher.tempVar.http.method.description="Received HTTP request method"
|
||||
AdvSceneSwitcher.tempVar.http.path="Request path"
|
||||
AdvSceneSwitcher.tempVar.http.path.description="Received HTTP request path"
|
||||
AdvSceneSwitcher.tempVar.http.body="Message body"
|
||||
AdvSceneSwitcher.tempVar.http.body.description="Received HTTP request body"
|
||||
|
||||
AdvSceneSwitcher.tempVar.mqtt.message="Message"
|
||||
|
||||
@@ -2354,6 +2490,14 @@ AdvSceneSwitcher.tempVar.cursor.y="Cursor position (Y)"
|
||||
AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path"
|
||||
AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved."
|
||||
|
||||
AdvSceneSwitcher.tempVar.sequence.macro="Macro"
|
||||
AdvSceneSwitcher.tempVar.sequence.macro.description="The name of the macro executed by the \"Sequence\" action.\nIf no macro was executed the returned value is the empty string."
|
||||
AdvSceneSwitcher.tempVar.sequence.nextMacro="Next Macro"
|
||||
AdvSceneSwitcher.tempVar.sequence.nextMacro.description="The name of the macro which will be executed next after the index was set.\nIf no macro will be executed the returned value is the empty string."
|
||||
|
||||
AdvSceneSwitcher.tempVar.random.macro="Macro"
|
||||
AdvSceneSwitcher.tempVar.random.macro.description="The name of the macro executed by the \"Random\" action.\nIf no macro was executed the returned value is the empty string."
|
||||
|
||||
AdvSceneSwitcher.selectScene="--select scene--"
|
||||
AdvSceneSwitcher.selectCanvas="--select canvas--"
|
||||
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
|
||||
|
||||
@@ -117,9 +117,7 @@ AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Durante la tra
|
||||
AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Durante la transición, verifique la escena de origen de la transición"
|
||||
AdvSceneSwitcher.condition.window="Ventana"
|
||||
AdvSceneSwitcher.condition.file="Archivo"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.file.layout="Contenido de{{filePath}}{{conditions}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.media="Medios"
|
||||
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
|
||||
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
|
||||
@@ -334,7 +332,6 @@ AdvSceneSwitcher.action.recording.type.start="Iniciar grabación"
|
||||
AdvSceneSwitcher.action.recording.type.pause="Pausar grabación"
|
||||
AdvSceneSwitcher.action.recording.type.unpause="Reanudar grabación"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Tenga en cuenta que, dependiendo de la configuración de grabación, es posible que no pueda pausar la grabación"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Búfer de reproducción"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Advertencia: ¡Guardar con demasiada frecuencia puede hacer que el búfer de reproducción no se guarde realmente!"
|
||||
AdvSceneSwitcher.action.replay.type.stop="Detener el búfer de reproducción"
|
||||
@@ -423,9 +420,9 @@ AdvSceneSwitcher.action.transition.type.scene="transición de escena"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="anulación de transición de escena"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Modificar {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
|
||||
AdvSceneSwitcher.action.transition.layout.type="Modificar {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
|
||||
AdvSceneSwitcher.action.timer="Temporizador"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Pausa"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Continuar"
|
||||
|
||||
@@ -187,8 +187,6 @@ AdvSceneSwitcher.condition.file="Fichier"
|
||||
AdvSceneSwitcher.condition.file.type.match="correspond à"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="a changé de date de modification"
|
||||
AdvSceneSwitcher.condition.file.remote="Fichier distant"
|
||||
AdvSceneSwitcher.condition.file.local="Fichier local"
|
||||
AdvSceneSwitcher.condition.media="Média"
|
||||
AdvSceneSwitcher.condition.media.source="Source"
|
||||
AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène"
|
||||
@@ -622,9 +620,9 @@ AdvSceneSwitcher.action.transition.type.scene="Transition de scène"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="Remplacement de la transition de scène"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="Transition de masquage de la source"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Modifier{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Définir le type de transition sur{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
|
||||
AdvSceneSwitcher.action.transition.layout.type="Modifier{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Définir le type de transition sur{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
|
||||
AdvSceneSwitcher.action.timer="Minuterie"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Continuer"
|
||||
|
||||
@@ -331,14 +331,6 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}ウィンドウにテ
|
||||
AdvSceneSwitcher.condition.window.entry.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。"
|
||||
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
|
||||
AdvSceneSwitcher.condition.file="ファイル"
|
||||
; AdvSceneSwitcher.condition.file.type.match="matches"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="内容変更しました"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="更新日変更"
|
||||
AdvSceneSwitcher.condition.file.remote="リモートファイル"
|
||||
AdvSceneSwitcher.condition.file.local="ローカルファイル"
|
||||
; AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
; AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
; AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
; AdvSceneSwitcher.condition.media="Media"
|
||||
AdvSceneSwitcher.condition.media.checkType.state="状態が一致"
|
||||
AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
|
||||
@@ -1020,9 +1012,9 @@ AdvSceneSwitcher.action.transition="トランジション"
|
||||
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
|
||||
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
|
||||
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
|
||||
; AdvSceneSwitcher.action.transition.entry.line1="{{type}}{{scenes}}{{sources}} の変更"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}移行時間を {{duration}} 秒に設定します"
|
||||
; AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}} の変更"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}移行時間を {{duration}} 秒に設定します"
|
||||
AdvSceneSwitcher.action.timer="タイマー"
|
||||
AdvSceneSwitcher.action.timer.type.pause="一時停止"
|
||||
AdvSceneSwitcher.action.timer.type.continue="続行"
|
||||
@@ -1190,8 +1182,15 @@ AdvSceneSwitcher.action.twitch.reward.toggleControl="リワード名/変数選
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません!"
|
||||
; AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.chat="{{channel}}でアカウント{{account}}{{actions}}を使用しています"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo="{{userInfoQueryType}}{{userLogin}}{{userId}}でアカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="チャンネル{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}でアカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="アカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="アカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="アカウント{{account}}{{actions}}を使用 タイムアウト{{duration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}} 理由{{banReason}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.title.title="タイトルを入力"
|
||||
AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明"
|
||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"
|
||||
|
||||
@@ -295,11 +295,6 @@ AdvSceneSwitcher.condition.file="Arquivo"
|
||||
AdvSceneSwitcher.condition.file.type.match="corresponde"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="data de modificação mudou"
|
||||
AdvSceneSwitcher.condition.file.remote="Arquivo remoto"
|
||||
AdvSceneSwitcher.condition.file.local="Arquivo local"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.media="Mídia"
|
||||
AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde"
|
||||
AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo corresponde"
|
||||
@@ -754,7 +749,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="Alterar pasta de saí
|
||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Alterar formatação do nome do arquivo"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Observe que dependendo das suas configurações de gravação, você pode não conseguir pausar a gravação"
|
||||
AdvSceneSwitcher.action.recording.split.hint="Certifique-se de habilitar a divisão automática de arquivos nas configurações do OBS primeiro!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Buffer de replay"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Aviso: Salvar com muita frequência pode resultar em o buffer de replay não ser realmente salvo!"
|
||||
AdvSceneSwitcher.action.replay.durationWarn="Aviso: Alterar o tempo máximo de replay só se aplicará na próxima vez que o buffer de replay for iniciado!"
|
||||
@@ -933,9 +927,9 @@ AdvSceneSwitcher.action.transition.type.scene="transição de cena"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="substituição de transição de cena"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="transição de exibição de fonte"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="transição de ocultação de fonte"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="Modificar{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Definir tipo de transição para{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Definir duração da transição para{{duration}}segundos"
|
||||
AdvSceneSwitcher.action.transition.layout.type="Modificar{{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Definir tipo de transição para{{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Definir duração da transição para{{duration}}segundos"
|
||||
AdvSceneSwitcher.action.timer="Temporizador"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Pausar"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Continuar"
|
||||
@@ -1065,7 +1059,7 @@ AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Habilitar modo apenas
|
||||
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Desabilitar modo apenas emotes no chat"
|
||||
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de chat"
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Não é possível selecionar categoria sem selecionar uma conta Twitch primeiro!"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.chat="Usando conta{{account}}{{actions}}em{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.title.title="Digite o título"
|
||||
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"
|
||||
|
||||
@@ -85,8 +85,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
|
||||
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
|
||||
AdvSceneSwitcher.condition.window="Окно"
|
||||
AdvSceneSwitcher.condition.file="Файл"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.media="Медиа"
|
||||
AdvSceneSwitcher.condition.video="Видео"
|
||||
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
|
||||
@@ -130,7 +128,6 @@ AdvSceneSwitcher.action.recording.type.start="Начать запись"
|
||||
AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
|
||||
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Буфер воспроизведения"
|
||||
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
|
||||
AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера"
|
||||
|
||||
@@ -107,9 +107,7 @@ AdvSceneSwitcher.condition.scene.type.notChanged="Sahne değişmedi"
|
||||
AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Geçiş hedefi sahnesi için geçiş kontrolü sırasında"
|
||||
AdvSceneSwitcher.condition.window="Pencere"
|
||||
AdvSceneSwitcher.condition.file="Dosya"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.file.layout="İçerik{{filePath}}{{conditions}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.media="Medya"
|
||||
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
|
||||
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
|
||||
@@ -265,7 +263,6 @@ AdvSceneSwitcher.action.recording.type.start="Kayıt Başlat"
|
||||
AdvSceneSwitcher.action.recording.type.pause="Kayıt Duraklat"
|
||||
AdvSceneSwitcher.action.recording.type.unpause="Kayıt Duraklatma"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Kayıt ayarlarınıza bağlı olarak kaydı duraklatamayabileceğinizi unutmayın."
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Tekrar arabelleği"
|
||||
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
|
||||
AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat"
|
||||
@@ -341,8 +338,8 @@ AdvSceneSwitcher.action.file.type.write="Yaz"
|
||||
AdvSceneSwitcher.action.file.type.append="Ekle"
|
||||
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
|
||||
AdvSceneSwitcher.action.transition="Geçiş"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
|
||||
AdvSceneSwitcher.action.timer="Zamanlayıcı"
|
||||
AdvSceneSwitcher.action.timer.type.pause="Duraklat"
|
||||
AdvSceneSwitcher.action.timer.type.continue="Devam et"
|
||||
|
||||
@@ -318,11 +318,7 @@ AdvSceneSwitcher.condition.file="文件"
|
||||
AdvSceneSwitcher.condition.file.type.match="内容匹配"
|
||||
AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变"
|
||||
AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变"
|
||||
AdvSceneSwitcher.condition.file.remote="网络文件"
|
||||
AdvSceneSwitcher.condition.file.local="本地文件"
|
||||
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
||||
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.media="媒体"
|
||||
AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
|
||||
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
|
||||
@@ -813,7 +809,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="更改输出文件夹
|
||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
|
||||
AdvSceneSwitcher.action.recording.split.hint="注意,请先确保在OBS设置中启用自动分割文件!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="回放缓存"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
|
||||
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
|
||||
@@ -994,9 +989,9 @@ AdvSceneSwitcher.action.transition.type.scene="场景转场动画"
|
||||
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
|
||||
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
|
||||
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
|
||||
AdvSceneSwitcher.action.transition.entry.line1="更改 {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}将转场动画类型设置为 {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
|
||||
AdvSceneSwitcher.action.transition.layout.type="更改 {{type}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}将转场动画类型设置为 {{transitions}}"
|
||||
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
|
||||
AdvSceneSwitcher.action.timer="计时器"
|
||||
AdvSceneSwitcher.action.timer.type.pause="暂停"
|
||||
AdvSceneSwitcher.action.timer.type.continue="继续"
|
||||
@@ -1139,10 +1134,17 @@ AdvSceneSwitcher.action.twitch.type.user.getInfo="获取用户信息"
|
||||
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
|
||||
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="在未选择 Twitch 帐户的情况下无法选择类别!"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.chat="使用账户{{account}}{{actions}}{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo="使用账户{{account}}{{actions}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="使用账户{{account}}{{actions}} 频道{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="使用账户{{account}}{{actions}}频道{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="使用账户{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="使用账户{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="使用账户{{account}}{{actions}}超时{{duration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}原因{{banReason}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="使用账户{{account}}{{actions}} 频道{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.title.title="输入标题"
|
||||
AdvSceneSwitcher.action.twitch.marker.description="标记描述"
|
||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"
|
||||
|
||||
@@ -251,6 +251,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="suppressCrashRecoveryDialog">
|
||||
<property name="text">
|
||||
<string>AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -597,7 +604,7 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<widget class="QGroupBox" name="macroListBox">
|
||||
<property name="title">
|
||||
<string>AdvSceneSwitcher.macroTab.macros</string>
|
||||
</property>
|
||||
|
||||
@@ -66,6 +66,7 @@ public slots:
|
||||
void on_uiHintsDisable_stateChanged(int state);
|
||||
void on_disableComboBoxFilter_stateChanged(int state);
|
||||
void on_warnPluginLoadFailure_stateChanged(int state);
|
||||
void on_suppressCrashRecoveryDialog_stateChanged(int state);
|
||||
void on_hideLegacyTabs_stateChanged(int state);
|
||||
void on_priorityUp_clicked();
|
||||
void on_priorityDown_clicked();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "crash-handler.hpp"
|
||||
#include "file-selection.hpp"
|
||||
#include "filter-combo-box.hpp"
|
||||
#include "first-run-wizard.hpp"
|
||||
@@ -194,6 +195,15 @@ void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
|
||||
switcher->warnPluginLoadFailure = state;
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_suppressCrashRecoveryDialog_stateChanged(int state)
|
||||
{
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetSuppressCrashDialog(state);
|
||||
}
|
||||
|
||||
static bool isLegacyTab(const QString &name)
|
||||
{
|
||||
return name == obs_module_text(
|
||||
@@ -946,6 +956,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
|
||||
FilterComboBox::SetFilterBehaviourEnabled(
|
||||
!switcher->disableFilterComboboxFilter);
|
||||
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
||||
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
|
||||
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
||||
|
||||
populatePriorityFunctionList(ui->priorityList);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroActionMacro::id = "macro";
|
||||
@@ -118,6 +120,36 @@ bool MacroActionMacro::PerformAction()
|
||||
case Action::TOGGLE_ACTION:
|
||||
AdjustActionState(macro);
|
||||
break;
|
||||
case Action::GET_INFO: {
|
||||
SetTempVarValue(
|
||||
"conditionCount",
|
||||
std::to_string(macro->Conditions().size()));
|
||||
SetTempVarValue(
|
||||
"actionCount",
|
||||
std::to_string(macro->Actions().size()));
|
||||
SetTempVarValue(
|
||||
"elseActionCount",
|
||||
std::to_string(macro->ElseActions().size()));
|
||||
SetTempVarValue("paused",
|
||||
macro->Paused() ? "true" : "false");
|
||||
SetTempVarValue("runCount",
|
||||
std::to_string(macro->RunCount()));
|
||||
const auto lastRun = macro->GetLastExecutionTime();
|
||||
const bool hasRun =
|
||||
lastRun.time_since_epoch().count() != 0;
|
||||
const auto secondsSinceLastRun =
|
||||
hasRun ? std::chrono::duration_cast<
|
||||
std::chrono::seconds>(
|
||||
std::chrono::
|
||||
high_resolution_clock::
|
||||
now() -
|
||||
lastRun)
|
||||
.count()
|
||||
: -1;
|
||||
SetTempVarValue("secondsSinceLastRun",
|
||||
std::to_string(secondsSinceLastRun));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -175,6 +207,9 @@ void MacroActionMacro::LogAction() const
|
||||
case Action::NESTED_MACRO:
|
||||
ablog(LOG_INFO, "run nested macro");
|
||||
break;
|
||||
case Action::GET_INFO:
|
||||
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -202,8 +237,8 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
|
||||
bool MacroActionMacro::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_action = static_cast<MacroActionMacro::Action>(
|
||||
obs_data_get_int(obj, "action"));
|
||||
SetAction(static_cast<MacroActionMacro::Action>(
|
||||
obs_data_get_int(obj, "action")));
|
||||
_macro.Load(obj);
|
||||
_actionSelectionType = static_cast<SelectionType>(
|
||||
obs_data_get_int(obj, "actionSelectionType"));
|
||||
@@ -303,6 +338,56 @@ void MacroActionMacro::RunActions(Macro *actionMacro) const
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionMacro::SetAction(Action action)
|
||||
{
|
||||
_action = action;
|
||||
SetupTempVars();
|
||||
}
|
||||
|
||||
void MacroActionMacro::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
|
||||
if (_action != Action::GET_INFO) {
|
||||
return;
|
||||
}
|
||||
|
||||
AddTempvar(
|
||||
"conditionCount",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.conditionCount"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.conditionCount.description"));
|
||||
AddTempvar(
|
||||
"actionCount",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.actionCount"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.actionCount.description"));
|
||||
AddTempvar(
|
||||
"elseActionCount",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description"));
|
||||
AddTempvar(
|
||||
"paused",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.paused"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.paused.description"));
|
||||
AddTempvar(
|
||||
"runCount",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.runCount"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.runCount.description"));
|
||||
AddTempvar(
|
||||
"secondsSinceLastRun",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description"));
|
||||
}
|
||||
|
||||
static void populateActionSelection(QComboBox *list)
|
||||
{
|
||||
static const std::vector<std::pair<MacroActionMacro::Action, std::string>>
|
||||
@@ -327,6 +412,8 @@ static void populateActionSelection(QComboBox *list)
|
||||
"AdvSceneSwitcher.action.macro.type.enableAction"},
|
||||
{MacroActionMacro::Action::TOGGLE_ACTION,
|
||||
"AdvSceneSwitcher.action.macro.type.toggleAction"},
|
||||
{MacroActionMacro::Action::GET_INFO,
|
||||
"AdvSceneSwitcher.action.macro.type.getInfo"},
|
||||
};
|
||||
|
||||
for (const auto &[value, name] : actions) {
|
||||
@@ -496,7 +583,7 @@ void MacroActionMacroEdit::UpdateEntryData()
|
||||
}
|
||||
|
||||
_actions->setCurrentIndex(
|
||||
_actions->findData(static_cast<int>(_entryData->_action)));
|
||||
_actions->findData(static_cast<int>(_entryData->GetAction())));
|
||||
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
|
||||
static_cast<int>(_entryData->_actionSelectionType)));
|
||||
_actionIndex->SetValue(_entryData->_actionIndex);
|
||||
@@ -545,8 +632,8 @@ void MacroActionMacroEdit::MacroChanged(const QString &text)
|
||||
void MacroActionMacroEdit::ActionChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_action = static_cast<MacroActionMacro::Action>(
|
||||
_actions->itemData(idx).toInt());
|
||||
_entryData->SetAction(static_cast<MacroActionMacro::Action>(
|
||||
_actions->itemData(idx).toInt()));
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -665,7 +752,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
||||
|
||||
};
|
||||
|
||||
const auto action = _entryData->_action;
|
||||
const auto action = _entryData->GetAction();
|
||||
const char *layoutText = "";
|
||||
switch (action) {
|
||||
case MacroActionMacro::Action::PAUSE:
|
||||
@@ -674,6 +761,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
||||
case MacroActionMacro::Action::RESET_COUNTER:
|
||||
case MacroActionMacro::Action::STOP:
|
||||
case MacroActionMacro::Action::NESTED_MACRO:
|
||||
case MacroActionMacro::Action::GET_INFO:
|
||||
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
|
||||
break;
|
||||
case MacroActionMacro::Action::RUN_ACTIONS:
|
||||
|
||||
@@ -56,11 +56,14 @@ public:
|
||||
TOGGLE_ACTION,
|
||||
TOGGLE_PAUSE,
|
||||
NESTED_MACRO,
|
||||
GET_INFO,
|
||||
};
|
||||
|
||||
void SetAction(Action);
|
||||
Action GetAction() const { return _action; }
|
||||
|
||||
enum class SelectionType { INDEX, LABEL, ID };
|
||||
|
||||
Action _action = Action::NESTED_MACRO;
|
||||
SelectionType _actionSelectionType = SelectionType::INDEX;
|
||||
bool _useElseSection = false;
|
||||
IntVariable _actionIndex = 1;
|
||||
@@ -72,9 +75,12 @@ public:
|
||||
int _customWidgetHeight = 0;
|
||||
|
||||
private:
|
||||
void SetupTempVars();
|
||||
void RunActions(Macro *actionMacro) const;
|
||||
void AdjustActionState(Macro *) const;
|
||||
|
||||
Action _action = Action::NESTED_MACRO;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,14 @@ namespace advss {
|
||||
|
||||
const std::string MacroActionVariable::id = "variable";
|
||||
|
||||
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
|
||||
{
|
||||
if (!_tempVar.HasValidID()) {
|
||||
return {};
|
||||
}
|
||||
return {_tempVar};
|
||||
}
|
||||
|
||||
bool MacroActionVariable::_registered = MacroActionFactory::Register(
|
||||
MacroActionVariable::id,
|
||||
{MacroActionVariable::Create, MacroActionVariableEdit::Create,
|
||||
@@ -1450,6 +1458,7 @@ void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_tempVar = var;
|
||||
IncrementTempVarInUseGeneration();
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
bool PostLoad();
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; };
|
||||
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void SetSegmentIndexValue(int);
|
||||
|
||||
@@ -6,6 +6,14 @@ namespace advss {
|
||||
|
||||
const std::string MacroConditionTempVar::id = "temp_var";
|
||||
|
||||
std::vector<TempVariableRef> MacroConditionTempVar::GetTempVarRefs() const
|
||||
{
|
||||
if (!_tempVar.HasValidID()) {
|
||||
return {};
|
||||
}
|
||||
return {_tempVar};
|
||||
}
|
||||
|
||||
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
|
||||
MacroConditionTempVar::id,
|
||||
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
|
||||
@@ -266,6 +274,7 @@ void MacroConditionTempVarEdit::VariableChanged(const TempVariableRef &var)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_tempVar = var;
|
||||
IncrementTempVarInUseGeneration();
|
||||
}
|
||||
|
||||
void MacroConditionTempVarEdit::Variable2Changed(const QString &text)
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; };
|
||||
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroConditionTempVar>(m);
|
||||
|
||||
@@ -366,7 +366,7 @@ static void runSegmentHighlightChecksHelper(MacroSegmentList *list)
|
||||
// highlight its segments if required
|
||||
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
|
||||
if (macroAction &&
|
||||
macroAction->_action ==
|
||||
macroAction->GetAction() ==
|
||||
MacroActionMacro::Action::NESTED_MACRO) {
|
||||
continue;
|
||||
}
|
||||
@@ -1086,6 +1086,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
|
||||
HighlightAction(idx);
|
||||
ui->actionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->actionsList->ensureWidgetVisible(
|
||||
ui->actionsList->WidgetAt(currentActionIdx));
|
||||
});
|
||||
}
|
||||
|
||||
void MacroEdit::AddMacroAction(int idx)
|
||||
@@ -1389,6 +1394,11 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
|
||||
HighlightElseAction(idx);
|
||||
ui->elseActionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->elseActionsList->ensureWidgetVisible(
|
||||
ui->elseActionsList->WidgetAt(currentElseActionIdx));
|
||||
});
|
||||
}
|
||||
|
||||
void MacroEdit::AddMacroElseAction(int idx)
|
||||
@@ -1591,6 +1601,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
|
||||
HighlightCondition(idx);
|
||||
ui->conditionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->conditionsList->ensureWidgetVisible(
|
||||
ui->conditionsList->WidgetAt(currentConditionIdx));
|
||||
});
|
||||
}
|
||||
|
||||
void MacroEdit::on_conditionAdd_clicked()
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
std::vector<TempVariableRef> MacroSegment::GetTempVarRefs() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
|
||||
: _macro(m),
|
||||
_supportsVariableValue(supportsVariableValue)
|
||||
@@ -152,6 +157,16 @@ void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
|
||||
NotifyUIAboutTempVarChange(this);
|
||||
}
|
||||
|
||||
bool MacroSegment::IsTempVarInUse(const std::string &id) const
|
||||
{
|
||||
for (const auto &var : _tempVariables) {
|
||||
if (var.ID() == id) {
|
||||
return var.IsInUse();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MacroSegment::SetTempVarValue(const std::string &id,
|
||||
const std::string &value)
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
std::string GetCustomLabel() const { return _customLabel; }
|
||||
virtual bool Save(obs_data_t *obj) const = 0;
|
||||
virtual bool Load(obs_data_t *obj) = 0;
|
||||
virtual std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||
virtual bool PostLoad();
|
||||
virtual std::string GetShortDesc() const;
|
||||
virtual std::string GetId() const = 0;
|
||||
@@ -54,6 +55,7 @@ protected:
|
||||
void AddTempvar(const std::string &id, const std::string &name,
|
||||
const std::string &description = "");
|
||||
|
||||
bool IsTempVarInUse(const std::string &id) const;
|
||||
void SetTempVarValue(const std::string &id, const std::string &value);
|
||||
|
||||
template<typename T, typename = std::enable_if_t<
|
||||
@@ -64,6 +66,14 @@ protected:
|
||||
: std::string("false"));
|
||||
}
|
||||
|
||||
template<typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
|
||||
void SetTempVarValue(const std::string &id, F &&valueProvider)
|
||||
{
|
||||
if (IsTempVarInUse(id)) {
|
||||
SetTempVarValue(id, valueProvider());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void ClearAvailableTempvars();
|
||||
std::optional<const TempVariable>
|
||||
|
||||
@@ -670,18 +670,6 @@ void AdvSceneSwitcher::SetupMacroTab()
|
||||
SLOT(HighlightOnChange()));
|
||||
onChangeHighlightTimer.start();
|
||||
|
||||
// Reserve more space for macro edit area than for the macro list
|
||||
ui->macroListMacroEditSplitter->setStretchFactor(0, 1);
|
||||
ui->macroListMacroEditSplitter->setStretchFactor(1, 4);
|
||||
|
||||
if (switcher->saveWindowGeo) {
|
||||
if (shouldRestoreSplitter(
|
||||
switcher->macroListMacroEditSplitterPosition)) {
|
||||
ui->macroListMacroEditSplitter->setSizes(
|
||||
switcher->macroListMacroEditSplitterPosition);
|
||||
}
|
||||
}
|
||||
|
||||
SetupMacroSearchWidgets(ui->macroSearchLayout, ui->macroSearchText,
|
||||
ui->macroSearchClear, ui->macroSearchType,
|
||||
ui->macroSearchRegex,
|
||||
@@ -705,6 +693,26 @@ void AdvSceneSwitcher::SetupMacroTab()
|
||||
ui->actionTriggerMode->addItem(obs_module_text(name),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
|
||||
ui->macroListBox->setSizePolicy(QSizePolicy::Ignored,
|
||||
QSizePolicy::Preferred);
|
||||
ui->macroListBox->setMinimumWidth(0);
|
||||
ui->macroEditGroup->setSizePolicy(QSizePolicy::Ignored,
|
||||
QSizePolicy::Preferred);
|
||||
ui->macroEditGroup->setMinimumWidth(0);
|
||||
|
||||
if (shouldRestoreSplitter(
|
||||
switcher->macroListMacroEditSplitterPosition)) {
|
||||
ui->macroListMacroEditSplitter->setSizes(
|
||||
switcher->macroListMacroEditSplitterPosition);
|
||||
} else {
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
const auto totalWidth =
|
||||
ui->macroListMacroEditSplitter->width();
|
||||
ui->macroListMacroEditSplitter->setSizes(
|
||||
{totalWidth / 5, totalWidth * 4 / 5});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QLabel>
|
||||
#include <QMainWindow>
|
||||
#include <QTimer>
|
||||
|
||||
#include <thread>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -25,10 +26,29 @@ static constexpr bool handleUncleanShutdown = true;
|
||||
#endif
|
||||
|
||||
static bool wasCleanShutdown = false;
|
||||
static bool suppressCrashDialog = false;
|
||||
|
||||
bool GetSuppressCrashDialog()
|
||||
{
|
||||
return suppressCrashDialog;
|
||||
}
|
||||
|
||||
void SetSuppressCrashDialog(bool suppress)
|
||||
{
|
||||
suppressCrashDialog = suppress;
|
||||
}
|
||||
|
||||
static void setup();
|
||||
static bool setupDone = []() {
|
||||
AddPluginInitStep(setup);
|
||||
AddSaveStep([](obs_data_t *obj) {
|
||||
obs_data_set_bool(obj, "suppressCrashDialog",
|
||||
suppressCrashDialog);
|
||||
});
|
||||
AddLoadStep([](obs_data_t *obj) {
|
||||
suppressCrashDialog =
|
||||
obs_data_get_bool(obj, "suppressCrashDialog");
|
||||
});
|
||||
return true;
|
||||
}();
|
||||
|
||||
@@ -108,8 +128,39 @@ static bool wasUncleanShutdown()
|
||||
|
||||
static void askForStartupSkip()
|
||||
{
|
||||
bool skipStart = DisplayMessage(
|
||||
obs_module_text("AdvSceneSwitcher.crashDetected"), true, false);
|
||||
auto mainWindow =
|
||||
static_cast<QMainWindow *>(obs_frontend_get_main_window());
|
||||
auto dialog = new QDialog(mainWindow);
|
||||
dialog->setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
dialog->setWindowFlags(dialog->windowFlags() &
|
||||
~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
auto layout = new QVBoxLayout(dialog);
|
||||
auto label = new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.crashDetected"), dialog);
|
||||
label->setWordWrap(true);
|
||||
layout->addWidget(label);
|
||||
|
||||
auto checkbox = new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.crashDetected.suppressCheckbox"),
|
||||
dialog);
|
||||
layout->addWidget(checkbox);
|
||||
|
||||
auto buttonbox = new QDialogButtonBox(
|
||||
QDialogButtonBox::Yes | QDialogButtonBox::No, dialog);
|
||||
QObject::connect(buttonbox, &QDialogButtonBox::accepted, dialog,
|
||||
&QDialog::accept);
|
||||
QObject::connect(buttonbox, &QDialogButtonBox::rejected, dialog,
|
||||
&QDialog::reject);
|
||||
layout->addWidget(buttonbox);
|
||||
|
||||
dialog->setLayout(layout);
|
||||
|
||||
bool skipStart = dialog->exec() == QDialog::Accepted;
|
||||
suppressCrashDialog = checkbox->isChecked();
|
||||
dialog->deleteLater();
|
||||
|
||||
if (!skipStart) {
|
||||
StartPlugin();
|
||||
}
|
||||
@@ -121,6 +172,10 @@ bool ShouldSkipPluginStartOnUncleanShutdown()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (suppressCrashDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This function is called while the plugin settings are being loaded.
|
||||
// Blocking at this stage can cause issues such as OBS failing to start
|
||||
// or crashing.
|
||||
|
||||
@@ -4,4 +4,7 @@ namespace advss {
|
||||
|
||||
bool ShouldSkipPluginStartOnUncleanShutdown();
|
||||
|
||||
bool GetSuppressCrashDialog();
|
||||
void SetSuppressCrashDialog(bool suppress);
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent)
|
||||
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent,
|
||||
const QString &browseTitle)
|
||||
: QWidget(parent),
|
||||
_type(type),
|
||||
_browseTitle(browseTitle),
|
||||
_filePath(new VariableLineEdit(this)),
|
||||
_browseButton(
|
||||
new QPushButton(obs_module_text("AdvSceneSwitcher.browse")))
|
||||
@@ -55,11 +57,14 @@ void FileSelection::BrowseButtonClicked()
|
||||
QString defaultPath = ValidPathOrDesktop(_filePath->text());
|
||||
QString path;
|
||||
if (_type == FileSelection::Type::WRITE) {
|
||||
path = QFileDialog::getSaveFileName(this, "", defaultPath);
|
||||
path = QFileDialog::getSaveFileName(this, _browseTitle,
|
||||
defaultPath);
|
||||
} else if (_type == FileSelection::Type::READ) {
|
||||
path = QFileDialog::getOpenFileName(this, "", defaultPath);
|
||||
path = QFileDialog::getOpenFileName(this, _browseTitle,
|
||||
defaultPath);
|
||||
} else {
|
||||
path = QFileDialog::getExistingDirectory(this, "", defaultPath);
|
||||
path = QFileDialog::getExistingDirectory(this, _browseTitle,
|
||||
defaultPath);
|
||||
}
|
||||
|
||||
if (path.isEmpty()) {
|
||||
|
||||
@@ -19,7 +19,7 @@ public:
|
||||
|
||||
EXPORT
|
||||
FileSelection(FileSelection::Type type = FileSelection::Type::READ,
|
||||
QWidget *parent = 0);
|
||||
QWidget *parent = 0, const QString &browseTitle = "");
|
||||
EXPORT void SetPath(const StringVariable &);
|
||||
EXPORT void SetPath(const QString &);
|
||||
EXPORT QString GetPath() const;
|
||||
@@ -34,6 +34,7 @@ signals:
|
||||
|
||||
private:
|
||||
Type _type;
|
||||
QString _browseTitle;
|
||||
VariableLineEdit *_filePath;
|
||||
QPushButton *_browseButton;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,17 @@ ListControls::ListControls(QWidget *parent, bool reorder) : QToolBar(parent)
|
||||
}
|
||||
}
|
||||
|
||||
void ListControls::AddWidget(QWidget *widget)
|
||||
{
|
||||
addSeparator();
|
||||
addWidget(widget);
|
||||
}
|
||||
|
||||
void ListControls::AddSeparator()
|
||||
{
|
||||
addSeparator();
|
||||
}
|
||||
|
||||
void ListControls::AddActionHelper(const char *theme, const char *className,
|
||||
const char *tooltip,
|
||||
const std::function<void()> &signal)
|
||||
|
||||
@@ -10,6 +10,8 @@ class ADVSS_EXPORT ListControls final : public QToolBar {
|
||||
|
||||
public:
|
||||
ListControls(QWidget *parent = nullptr, bool reorder = true);
|
||||
void AddWidget(QWidget *widget);
|
||||
void AddSeparator();
|
||||
|
||||
signals:
|
||||
void Add();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#include "list-editor.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QEvent>
|
||||
|
||||
namespace advss {
|
||||
|
||||
ListEditor::ListEditor(QWidget *parent, bool reorder)
|
||||
: QWidget(parent),
|
||||
_list(new QListWidget()),
|
||||
_controls(new ListControls(this, reorder)),
|
||||
_mainLayout(new QVBoxLayout())
|
||||
_mainLayout(new QVBoxLayout()),
|
||||
_placeholder(new QLabel(_list->viewport()))
|
||||
{
|
||||
QWidget::connect(_controls, SIGNAL(Add()), this, SLOT(Add()));
|
||||
QWidget::connect(_controls, SIGNAL(Remove()), this, SLOT(Remove()));
|
||||
@@ -15,6 +18,19 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
|
||||
QWidget::connect(_controls, SIGNAL(Down()), this, SLOT(Down()));
|
||||
QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)),
|
||||
this, SLOT(Clicked(QListWidgetItem *)));
|
||||
QWidget::connect(_list->model(),
|
||||
SIGNAL(rowsInserted(QModelIndex, int, int)), this,
|
||||
SLOT(UpdatePlaceholder()));
|
||||
QWidget::connect(_list->model(),
|
||||
SIGNAL(rowsRemoved(QModelIndex, int, int)), this,
|
||||
SLOT(UpdatePlaceholder()));
|
||||
QWidget::connect(_list->model(), SIGNAL(modelReset()), this,
|
||||
SLOT(UpdatePlaceholder()));
|
||||
|
||||
_placeholder->setAlignment(Qt::AlignCenter);
|
||||
_placeholder->setWordWrap(true);
|
||||
_placeholder->hide();
|
||||
_list->viewport()->installEventFilter(this);
|
||||
|
||||
_mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_mainLayout->addWidget(_list);
|
||||
@@ -22,6 +38,40 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
|
||||
setLayout(_mainLayout);
|
||||
}
|
||||
|
||||
void ListEditor::SetPlaceholderText(const QString &text)
|
||||
{
|
||||
_placeholder->setText(text);
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
void ListEditor::SetMinListHeight(int value)
|
||||
{
|
||||
_minHeight = value;
|
||||
}
|
||||
|
||||
void ListEditor::SetMaxListHeight(int value)
|
||||
{
|
||||
_maxHeight = value;
|
||||
}
|
||||
|
||||
void ListEditor::UpdatePlaceholder()
|
||||
{
|
||||
bool visible = !_placeholder->text().isEmpty() && _list->count() == 0;
|
||||
_placeholder->setVisible(visible);
|
||||
if (visible) {
|
||||
_placeholder->setGeometry(_list->viewport()->rect());
|
||||
}
|
||||
}
|
||||
|
||||
bool ListEditor::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == _list->viewport() && event->type() == QEvent::Resize &&
|
||||
_placeholder->isVisible()) {
|
||||
_placeholder->setGeometry(_list->viewport()->rect());
|
||||
}
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void ListEditor::showEvent(QShowEvent *e)
|
||||
{
|
||||
QWidget::showEvent(e);
|
||||
@@ -64,6 +114,24 @@ int ListEditor::GetIndexOfSignal() const
|
||||
void ListEditor::UpdateListSize()
|
||||
{
|
||||
SetHeightToContentHeight(_list);
|
||||
|
||||
if (_list->count() == 0 && !_placeholder->text().isEmpty()) {
|
||||
auto height = _list->fontMetrics().height() * 3;
|
||||
_list->setMinimumHeight(height);
|
||||
_list->setMaximumHeight(height);
|
||||
}
|
||||
|
||||
if (_minHeight >= 0 && _list->minimumHeight() != _minHeight) {
|
||||
_list->setMinimumHeight(_minHeight);
|
||||
}
|
||||
|
||||
if (_maxHeight >= 0 && _list->maximumHeight() != _maxHeight) {
|
||||
if (_list->minimumHeight() > _maxHeight) {
|
||||
_list->setMinimumHeight(_maxHeight);
|
||||
}
|
||||
_list->setMaximumHeight(_maxHeight);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "export-symbol-helper.hpp"
|
||||
#include "list-controls.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QLayout>
|
||||
|
||||
@@ -13,9 +14,13 @@ class ADVSS_EXPORT ListEditor : public QWidget {
|
||||
public:
|
||||
ListEditor(QWidget *parent = nullptr, bool reorder = true);
|
||||
int count() const { return _list->count(); };
|
||||
void SetPlaceholderText(const QString &text);
|
||||
void SetMinListHeight(int);
|
||||
void SetMaxListHeight(int);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *);
|
||||
bool eventFilter(QObject *, QEvent *);
|
||||
|
||||
private slots:
|
||||
virtual void Add() = 0;
|
||||
@@ -23,6 +28,7 @@ private slots:
|
||||
virtual void Up(){};
|
||||
virtual void Down(){};
|
||||
virtual void Clicked(QListWidgetItem *) {}
|
||||
void UpdatePlaceholder();
|
||||
|
||||
protected:
|
||||
void UpdateListSize();
|
||||
@@ -31,6 +37,11 @@ protected:
|
||||
QListWidget *_list;
|
||||
ListControls *_controls;
|
||||
QVBoxLayout *_mainLayout;
|
||||
|
||||
private:
|
||||
QLabel *_placeholder;
|
||||
int _minHeight = -1;
|
||||
int _maxHeight = -1;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -9,19 +9,15 @@ namespace advss {
|
||||
|
||||
std::variant<double, std::string> EvalMathExpression(const std::string &expr)
|
||||
{
|
||||
static bool setupDone = false;
|
||||
static exprtk::symbol_table<double> symbolTable;
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<double> dis(0.0, 1.0);
|
||||
static auto randomFunc = []() {
|
||||
thread_local std::mt19937 gen(std::random_device{}());
|
||||
thread_local std::uniform_real_distribution<double> dis(0.0,
|
||||
1.0);
|
||||
return dis(gen);
|
||||
};
|
||||
|
||||
if (!setupDone) {
|
||||
symbolTable.add_function("random", randomFunc);
|
||||
setupDone = true;
|
||||
}
|
||||
exprtk::symbol_table<double> symbolTable;
|
||||
symbolTable.add_function("random", randomFunc);
|
||||
|
||||
exprtk::expression<double> expression;
|
||||
expression.register_symbol_table(symbolTable);
|
||||
|
||||
@@ -66,4 +66,15 @@ void Lockable::WithLock(const std::function<void()> &func)
|
||||
func();
|
||||
}
|
||||
|
||||
SuspendLock::SuspendLock(Lockable &lockable)
|
||||
: _mtx(static_cast<std::mutex &>(lockable._mtx))
|
||||
{
|
||||
_mtx.unlock();
|
||||
}
|
||||
|
||||
SuspendLock::~SuspendLock()
|
||||
{
|
||||
_mtx.lock();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -51,6 +51,24 @@ public:
|
||||
|
||||
private:
|
||||
PerInstanceMutex _mtx;
|
||||
|
||||
friend class SuspendLock;
|
||||
};
|
||||
|
||||
// RAII guard that temporarily releases a Lockable's per-segment lock.
|
||||
// Use this inside PerformAction() / CheckCondition() to unblock the UI
|
||||
// during long-running operations while still running on the original segment.
|
||||
// The caller MUST be holding the lock (i.e. be inside WithLock) when
|
||||
// constructing this object; the lock is re-acquired on destruction.
|
||||
class EXPORT SuspendLock {
|
||||
public:
|
||||
SuspendLock(Lockable &lockable);
|
||||
~SuspendLock();
|
||||
SuspendLock(const SuspendLock &) = delete;
|
||||
SuspendLock &operator=(const SuspendLock &) = delete;
|
||||
|
||||
private:
|
||||
std::mutex &_mtx;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
|
||||
#include <QVariant>
|
||||
#include <QAbstractItemView>
|
||||
#include <atomic>
|
||||
|
||||
Q_DECLARE_METATYPE(advss::TempVariableRef);
|
||||
|
||||
static std::atomic<uint64_t> tempVarInUseGeneration{0};
|
||||
|
||||
namespace advss {
|
||||
|
||||
TempVariable::TempVariable(const std::string &id, const std::string &name,
|
||||
@@ -120,6 +123,60 @@ TempVariableRef TempVariable::GetRef() const
|
||||
return ref;
|
||||
}
|
||||
|
||||
static bool refsContain(const std::vector<TempVariableRef> &refs,
|
||||
const TempVariableRef &ref)
|
||||
{
|
||||
for (const auto &r : refs) {
|
||||
if (r == ref) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool segmentsReferTo(const T &segments, const TempVariableRef &ref)
|
||||
{
|
||||
for (const auto &segment : segments) {
|
||||
if (refsContain(segment->GetTempVarRefs(), ref)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TempVariable::IsInUse() const
|
||||
{
|
||||
const auto currentGen =
|
||||
tempVarInUseGeneration.load(std::memory_order_relaxed);
|
||||
if (_isInUseCacheGeneration == currentGen) {
|
||||
return _isInUseCache;
|
||||
}
|
||||
|
||||
bool inUse = false;
|
||||
const auto ref = GetRef();
|
||||
|
||||
if (ref.HasValidID()) {
|
||||
for (const auto ¯o : GetAllMacros()) {
|
||||
if (segmentsReferTo(macro->Conditions(), ref) ||
|
||||
segmentsReferTo(macro->Actions(), ref) ||
|
||||
segmentsReferTo(macro->ElseActions(), ref)) {
|
||||
inUse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isInUseCache = inUse;
|
||||
_isInUseCacheGeneration = currentGen;
|
||||
return inUse;
|
||||
}
|
||||
|
||||
void IncrementTempVarInUseGeneration()
|
||||
{
|
||||
tempVarInUseGeneration.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TempVariableRef::SegmentType TempVariableRef::GetType() const
|
||||
{
|
||||
auto segment = _segment.lock();
|
||||
@@ -665,13 +722,13 @@ TempVarSignalManager *TempVarSignalManager::Instance()
|
||||
|
||||
void NotifyUIAboutTempVarChange(MacroSegment *segment)
|
||||
{
|
||||
obs_queue_task(
|
||||
OBS_TASK_UI,
|
||||
IncrementTempVarInUseGeneration();
|
||||
QueueUITask(
|
||||
[](void *segment) {
|
||||
TempVarSignalManager::Instance()->SegmentTempVarsChanged(
|
||||
(MacroSegment *)segment);
|
||||
},
|
||||
segment, false);
|
||||
segment);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
void SetValue(const std::string &val);
|
||||
void InvalidateValue();
|
||||
TempVariableRef GetRef() const;
|
||||
EXPORT bool IsInUse() const;
|
||||
|
||||
private:
|
||||
std::string _id = "";
|
||||
@@ -53,6 +54,8 @@ private:
|
||||
mutable std::mutex _lastValuesMutex;
|
||||
std::vector<std::string> _lastValues;
|
||||
bool _valueIsValid = false;
|
||||
mutable bool _isInUseCache = false;
|
||||
mutable uint64_t _isInUseCacheGeneration = UINT64_MAX;
|
||||
|
||||
std::weak_ptr<MacroSegment> _segment;
|
||||
friend TempVariableSelection;
|
||||
@@ -122,5 +125,6 @@ private:
|
||||
};
|
||||
|
||||
void NotifyUIAboutTempVarChange(MacroSegment *);
|
||||
EXPORT void IncrementTempVarInUseGeneration();
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -9,11 +9,12 @@ void StringVariable::Resolve() const
|
||||
_resolvedValue = _value;
|
||||
return;
|
||||
}
|
||||
if (_lastResolve == GetLastVariableChangeTime()) {
|
||||
const auto lastChange = GetLastVariableChangeTime();
|
||||
if (_lastResolve == lastChange) {
|
||||
return;
|
||||
}
|
||||
_resolvedValue = SubstitueVariables(_value);
|
||||
_lastResolve = GetLastVariableChangeTime();
|
||||
_lastResolve = lastChange;
|
||||
}
|
||||
|
||||
StringVariable::operator std::string() const
|
||||
@@ -81,7 +82,7 @@ std::string SubstitueVariables(std::string str)
|
||||
const auto &variable = std::dynamic_pointer_cast<Variable>(v);
|
||||
const std::string pattern = "${" + variable->Name() + "}";
|
||||
if (ReplaceAll(str, pattern, variable->Value(false))) {
|
||||
variable->UpdateLastUsed();
|
||||
variable->MarkAsUsed();
|
||||
}
|
||||
}
|
||||
return str;
|
||||
|
||||
@@ -12,16 +12,23 @@ static std::deque<std::shared_ptr<Item>> variables;
|
||||
|
||||
// Keep track of the last time a variable was changed to save some work when
|
||||
// when resolving strings containing variables, etc.
|
||||
static std::mutex lastVariableChangeMutex;
|
||||
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
||||
|
||||
static void setLastVariableChangeTime()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
Variable::Variable() : Item()
|
||||
{
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
Variable::~Variable()
|
||||
{
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
void Variable::Load(obs_data_t *obj)
|
||||
@@ -37,7 +44,7 @@ void Variable::Load(obs_data_t *obj)
|
||||
SetValue(_defaultValue);
|
||||
}
|
||||
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
void Variable::Save(obs_data_t *obj) const
|
||||
@@ -45,8 +52,11 @@ void Variable::Save(obs_data_t *obj) const
|
||||
Item::Save(obj);
|
||||
obs_data_set_int(obj, "saveAction", static_cast<int>(_saveAction));
|
||||
|
||||
if (_saveAction == SaveAction::SAVE) {
|
||||
obs_data_set_string(obj, "value", _value.c_str());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_saveAction == SaveAction::SAVE) {
|
||||
obs_data_set_string(obj, "value", _value.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
obs_data_set_string(obj, "defaultValue", _defaultValue.c_str());
|
||||
@@ -62,6 +72,18 @@ std::string Variable::Value(bool updateLastUsed) const
|
||||
return _value;
|
||||
}
|
||||
|
||||
std::string Variable::GetPreviousValue() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return _previousValue;
|
||||
}
|
||||
|
||||
int Variable::GetValueChangeCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return _valueChangeCount;
|
||||
}
|
||||
|
||||
std::optional<double> Variable::DoubleValue() const
|
||||
{
|
||||
return GetDouble(Value());
|
||||
@@ -79,8 +101,11 @@ void Variable::SetValue(const std::string &value)
|
||||
_value = value;
|
||||
|
||||
UpdateLastUsed();
|
||||
UpdateLastChanged();
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
if (_previousValue != _value) {
|
||||
_lastChanged = std::chrono::high_resolution_clock::now();
|
||||
++_valueChangeCount;
|
||||
}
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
void Variable::SetValue(double value)
|
||||
@@ -90,6 +115,7 @@ void Variable::SetValue(double value)
|
||||
|
||||
std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_lastUsed.time_since_epoch().count() == 0) {
|
||||
return {};
|
||||
}
|
||||
@@ -101,6 +127,7 @@ std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
|
||||
|
||||
std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_lastChanged.time_since_epoch().count() == 0) {
|
||||
return {};
|
||||
}
|
||||
@@ -116,12 +143,10 @@ void Variable::UpdateLastUsed() const
|
||||
_lastUsed = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
void Variable::UpdateLastChanged()
|
||||
void Variable::MarkAsUsed() const
|
||||
{
|
||||
if (_previousValue != _value) {
|
||||
_lastChanged = std::chrono::high_resolution_clock::now();
|
||||
++_valueChangeCount;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
UpdateLastUsed();
|
||||
}
|
||||
|
||||
static void populateSaveActionSelection(QComboBox *list)
|
||||
@@ -206,7 +231,7 @@ bool VariableSettingsDialog::AskForSettings(QWidget *parent, Variable &settings)
|
||||
dialog._defaultValue->toPlainText().toStdString();
|
||||
settings._saveAction =
|
||||
static_cast<Variable::SaveAction>(dialog._save->currentIndex());
|
||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
||||
setLastVariableChangeTime();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -461,6 +486,7 @@ void ImportVariables(obs_data_t *data)
|
||||
|
||||
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
|
||||
return lastVariableChange;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,16 +34,15 @@ public:
|
||||
EXPORT std::string Value(bool updateLastUsed = true) const;
|
||||
EXPORT std::optional<double> DoubleValue() const;
|
||||
EXPORT std::optional<int> IntValue() const;
|
||||
std::string GetPreviousValue() const { return _previousValue; };
|
||||
std::string GetPreviousValue() const;
|
||||
std::string GetDefaultValue() const { return _defaultValue; }
|
||||
EXPORT void SetValue(const std::string &value);
|
||||
void SetValue(double value);
|
||||
SaveAction GetSaveAction() const { return _saveAction; }
|
||||
int GetValueChangeCount() const { return _valueChangeCount; }
|
||||
int GetValueChangeCount() const;
|
||||
std::optional<uint64_t> GetSecondsSinceLastUse() const;
|
||||
std::optional<uint64_t> GetSecondsSinceLastChange() const;
|
||||
void UpdateLastUsed() const;
|
||||
void UpdateLastChanged();
|
||||
void MarkAsUsed() const;
|
||||
|
||||
private:
|
||||
SaveAction _saveAction = SaveAction::DONT_SAVE;
|
||||
@@ -55,6 +54,8 @@ private:
|
||||
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
|
||||
mutable std::mutex _mutex;
|
||||
|
||||
void UpdateLastUsed() const;
|
||||
|
||||
friend VariableSelection;
|
||||
friend VariableSettingsDialog;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,8 @@ target_sources(
|
||||
macro-action-obs-settings.hpp
|
||||
macro-action-osc.cpp
|
||||
macro-action-osc.hpp
|
||||
macro-action-play-audio.cpp
|
||||
macro-action-play-audio.hpp
|
||||
macro-action-plugin-state.cpp
|
||||
macro-action-plugin-state.hpp
|
||||
macro-action-profile.cpp
|
||||
@@ -57,6 +59,8 @@ target_sources(
|
||||
macro-action-sequence.hpp
|
||||
macro-action-source.cpp
|
||||
macro-action-source.hpp
|
||||
macro-action-source-interaction.cpp
|
||||
macro-action-source-interaction.hpp
|
||||
macro-action-streaming.cpp
|
||||
macro-action-streaming.hpp
|
||||
macro-action-studio-mode.cpp
|
||||
@@ -144,7 +148,17 @@ target_sources(
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE utils/audio-helpers.cpp
|
||||
PRIVATE utils/source-interaction-recorder.cpp
|
||||
utils/source-interaction-recorder.hpp
|
||||
utils/source-interaction-step.cpp
|
||||
utils/source-interaction-step.hpp
|
||||
utils/source-interaction-step-edit.cpp
|
||||
utils/source-interaction-step-edit.hpp
|
||||
utils/source-interaction-step-list.cpp
|
||||
utils/source-interaction-step-list.hpp
|
||||
utils/source-preview-widget.cpp
|
||||
utils/source-preview-widget.hpp
|
||||
utils/audio-helpers.cpp
|
||||
utils/audio-helpers.hpp
|
||||
utils/connection-manager.cpp
|
||||
utils/connection-manager.hpp
|
||||
|
||||
@@ -9,6 +9,14 @@ namespace advss {
|
||||
|
||||
const std::string MacroActionFilter::id = "filter";
|
||||
|
||||
std::vector<TempVariableRef> MacroActionFilter::GetTempVarRefs() const
|
||||
{
|
||||
if (!_tempVar.HasValidID()) {
|
||||
return {};
|
||||
}
|
||||
return {_tempVar};
|
||||
}
|
||||
|
||||
bool MacroActionFilter::_registered = MacroActionFactory::Register(
|
||||
MacroActionFilter::id,
|
||||
{MacroActionFilter::Create, MacroActionFilterEdit::Create,
|
||||
@@ -428,6 +436,7 @@ void MacroActionFilterEdit::SelectionChanged(const TempVariableRef &var)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_tempVar = var;
|
||||
IncrementTempVarInUseGeneration();
|
||||
}
|
||||
|
||||
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)
|
||||
|
||||
@@ -41,6 +41,8 @@ public:
|
||||
SettingsInputMethod _settingsInputMethod =
|
||||
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
||||
|
||||
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||
|
||||
SourceSelection _source;
|
||||
FilterSelection _filter;
|
||||
Action _action = Action::ENABLE;
|
||||
|
||||
@@ -95,7 +95,7 @@ static void waitHelper(std::unique_lock<std::mutex> *lock, Macro *macro,
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
|
||||
void MacroActionMedia::PerformActionHelper(obs_source_t *source)
|
||||
{
|
||||
obs_media_state state = obs_source_media_get_state(source);
|
||||
|
||||
@@ -130,6 +130,7 @@ void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
|
||||
SeekToPercentage(source);
|
||||
break;
|
||||
case Action::WAIT_FOR_PLAYBACK_STOP: {
|
||||
SuspendLock suspendLock(*this);
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
waitHelper(&lock, GetMacro(), source);
|
||||
break;
|
||||
|
||||
@@ -45,7 +45,7 @@ public:
|
||||
SceneSelection _scene;
|
||||
|
||||
private:
|
||||
void PerformActionHelper(obs_source_t *) const;
|
||||
void PerformActionHelper(obs_source_t *);
|
||||
void SeekToPercentage(obs_source_t *source) const;
|
||||
|
||||
static bool _registered;
|
||||
|
||||
446
plugins/base/macro-action-play-audio.cpp
Normal file
446
plugins/base/macro-action-play-audio.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "macro-action-play-audio.hpp"
|
||||
#include "audio-helpers.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Use a high output channel index that is unlikely to be claimed by OBS or
|
||||
// other plugins. OBS supports channels 0-63; channel 0 is the main scene.
|
||||
static constexpr uint32_t kPlaybackOutputChannel = 63;
|
||||
|
||||
const std::string MacroActionPlayAudio::id = "play_audio";
|
||||
|
||||
bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
|
||||
MacroActionPlayAudio::id,
|
||||
{MacroActionPlayAudio::Create, MacroActionPlayAudioEdit::Create,
|
||||
"AdvSceneSwitcher.action.playAudio"});
|
||||
|
||||
static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
|
||||
{
|
||||
if (wantsOutput) {
|
||||
obs_set_output_source(kPlaybackOutputChannel, nullptr);
|
||||
} else {
|
||||
obs_source_dec_active(source);
|
||||
}
|
||||
}
|
||||
|
||||
static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
|
||||
int64_t maxMs = 0)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
SetMacroAbortWait(false);
|
||||
|
||||
// The media source needs time to open and decode before reaching
|
||||
// PLAYING state. Poll until it starts (or the macro is stopped).
|
||||
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
|
||||
if (obs_source_media_get_state(source) ==
|
||||
OBS_MEDIA_STATE_PLAYING) {
|
||||
break;
|
||||
}
|
||||
GetMacroWaitCV().wait_for(lock, 10ms);
|
||||
}
|
||||
|
||||
// Now wait for playback to end. Require two consecutive non-playing
|
||||
// samples to avoid false positives on brief state transitions.
|
||||
// If maxMs > 0, also stop once that many milliseconds have elapsed.
|
||||
const auto playbackStart = std::chrono::steady_clock::now();
|
||||
static const int kStopThreshold = 2;
|
||||
int stoppedCount = 0;
|
||||
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
|
||||
if (maxMs > 0) {
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<
|
||||
std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() -
|
||||
playbackStart)
|
||||
.count();
|
||||
if (elapsed >= maxMs) {
|
||||
obs_source_media_stop(source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (obs_source_media_get_state(source) !=
|
||||
OBS_MEDIA_STATE_PLAYING) {
|
||||
if (++stoppedCount >= kStopThreshold) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
stoppedCount = 0;
|
||||
}
|
||||
GetMacroWaitCV().wait_for(lock, 10ms);
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionPlayAudio::PerformAction()
|
||||
{
|
||||
std::string path = _filePath;
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!QFileInfo::exists(QString::fromStdString(path))) {
|
||||
blog(LOG_WARNING, "audio file not found: \"%s\"", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease settings = obs_data_create();
|
||||
obs_data_set_string(settings, "local_file", path.c_str());
|
||||
obs_data_set_bool(settings, "is_local_file", true);
|
||||
obs_data_set_bool(settings, "looping", false);
|
||||
// Disable automatic restart on activate so we control start explicitly.
|
||||
obs_data_set_bool(settings, "restart_on_activate", false);
|
||||
obs_data_set_bool(settings, "close_when_inactive", true);
|
||||
|
||||
OBSSourceAutoRelease source = obs_source_create_private(
|
||||
"ffmpeg_source", "advss_play_audio", settings);
|
||||
|
||||
if (!source) {
|
||||
blog(LOG_WARNING,
|
||||
"Failed to create ffmpeg_source for audio playback of \"%s\"",
|
||||
path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
const float vol =
|
||||
DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
|
||||
obs_source_set_volume(source, vol);
|
||||
obs_source_set_monitoring_type(source, _monitorType);
|
||||
|
||||
// Fall back to monitor-only if all output tracks are deselected —
|
||||
// there is no point routing through the output channel if the mixer
|
||||
// mask would silence every track.
|
||||
const bool wantsOutput =
|
||||
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
|
||||
(_audioMixers != 0);
|
||||
if (!wantsOutput &&
|
||||
_monitorType == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT) {
|
||||
obs_source_set_monitoring_type(
|
||||
source, OBS_MONITORING_TYPE_MONITOR_ONLY);
|
||||
}
|
||||
|
||||
if (wantsOutput) {
|
||||
obs_source_set_audio_mixers(source, _audioMixers);
|
||||
// Route through a private scene so we can position the scene
|
||||
// item far off-screen. This keeps the item "visible" (so audio
|
||||
// is still mixed) while ensuring its video never intersects the
|
||||
// output frame.
|
||||
OBSSceneAutoRelease audioScene =
|
||||
obs_scene_create_private("advss_audio_scene");
|
||||
obs_sceneitem_t *item = obs_scene_add(audioScene, source);
|
||||
if (item) {
|
||||
struct vec2 pos = {-99999.0f, -99999.0f};
|
||||
obs_sceneitem_set_pos(item, &pos);
|
||||
}
|
||||
obs_set_output_source(kPlaybackOutputChannel,
|
||||
obs_scene_get_source(audioScene));
|
||||
// audioScene released here; the output channel holds the
|
||||
// remaining reference and keeps the scene alive.
|
||||
} else {
|
||||
obs_source_set_audio_mixers(source, 0);
|
||||
obs_source_inc_active(source);
|
||||
}
|
||||
|
||||
if (_useStartOffset && _startOffset.Milliseconds() > 0) {
|
||||
obs_source_media_set_time(source, _startOffset.Milliseconds());
|
||||
}
|
||||
|
||||
obs_source_media_play_pause(source, false);
|
||||
|
||||
const int64_t maxMs = _useDuration ? _playbackDuration.Milliseconds()
|
||||
: 0;
|
||||
|
||||
if (_waitForCompletion) {
|
||||
SetMacroAbortWait(false);
|
||||
{
|
||||
SuspendLock suspendLock(*this);
|
||||
waitForPlaybackToEnd(GetMacro(), source, maxMs);
|
||||
}
|
||||
deactivatePlayback(source, wantsOutput);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep the source alive in a background thread that cleans up
|
||||
// once playback finishes. Grab an extra strong reference so the
|
||||
// source survives beyond this stack frame.
|
||||
auto rawSource = obs_source_get_ref(source);
|
||||
auto macro = GetMacro();
|
||||
|
||||
std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
|
||||
waitForPlaybackToEnd(macro, rawSource, maxMs);
|
||||
deactivatePlayback(rawSource, wantsOutput);
|
||||
obs_source_release(rawSource);
|
||||
});
|
||||
AddMacroHelperThread(macro, std::move(cleanupThread));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudio::LogAction() const
|
||||
{
|
||||
ablog(LOG_INFO,
|
||||
"playing audio file \"%s\" at %.1f dB (monitoring type %d, wait %d)",
|
||||
_filePath.UnresolvedValue().c_str(),
|
||||
static_cast<double>(_volumeDB.GetFixedValue()), _monitorType,
|
||||
(int)_waitForCompletion);
|
||||
}
|
||||
|
||||
bool MacroActionPlayAudio::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
_filePath.Save(obj, "filePath");
|
||||
_volumeDB.Save(obj, "volumeDB");
|
||||
obs_data_set_int(obj, "monitorType", _monitorType);
|
||||
obs_data_set_int(obj, "audioMixers", _audioMixers);
|
||||
obs_data_set_bool(obj, "useStartOffset", _useStartOffset);
|
||||
_startOffset.Save(obj, "startOffset");
|
||||
obs_data_set_bool(obj, "useDuration", _useDuration);
|
||||
_playbackDuration.Save(obj, "playbackDuration");
|
||||
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionPlayAudio::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_filePath.Load(obj, "filePath");
|
||||
_volumeDB.Load(obj, "volumeDB");
|
||||
_monitorType = static_cast<obs_monitoring_type>(
|
||||
obs_data_get_int(obj, "monitorType"));
|
||||
_audioMixers =
|
||||
static_cast<uint32_t>(obs_data_get_int(obj, "audioMixers"));
|
||||
_useStartOffset = obs_data_get_bool(obj, "useStartOffset");
|
||||
_startOffset.Load(obj, "startOffset");
|
||||
_useDuration = obs_data_get_bool(obj, "useDuration");
|
||||
_playbackDuration.Load(obj, "playbackDuration");
|
||||
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroActionPlayAudio::GetShortDesc() const
|
||||
{
|
||||
return _filePath.UnresolvedValue();
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionPlayAudio::Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionPlayAudio>(m);
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionPlayAudio::Copy() const
|
||||
{
|
||||
return std::make_shared<MacroActionPlayAudio>(*this);
|
||||
}
|
||||
|
||||
void MacroActionPlayAudio::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_filePath.ResolveVariables();
|
||||
_volumeDB.ResolveVariables();
|
||||
_startOffset.ResolveVariables();
|
||||
_playbackDuration.ResolveVariables();
|
||||
}
|
||||
|
||||
MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionPlayAudio> entryData)
|
||||
: QWidget(parent),
|
||||
_filePath(new FileSelection(
|
||||
FileSelection::Type::READ, this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.file.browse"))),
|
||||
_volumeDB(new VariableDoubleSpinBox),
|
||||
_monitorTypes(new QComboBox),
|
||||
_tracksContainer(new QWidget),
|
||||
_useStartOffset(new QCheckBox(this)),
|
||||
_startOffset(new DurationSelection(this, true, 0.0)),
|
||||
_useDuration(new QCheckBox(this)),
|
||||
_playbackDuration(new DurationSelection(this, true, 0.0)),
|
||||
_waitForCompletion(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.action.playAudio.wait")))
|
||||
{
|
||||
_volumeDB->setMinimum(-100.0);
|
||||
_volumeDB->setMaximum(0.0);
|
||||
_volumeDB->setSuffix("dB");
|
||||
_volumeDB->setSpecialValueText("-inf");
|
||||
|
||||
if (obs_audio_monitoring_available()) {
|
||||
PopulateMonitorTypeSelection(_monitorTypes);
|
||||
} else {
|
||||
_monitorTypes->addItem(obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.monitorUnavailable"));
|
||||
_monitorTypes->setEnabled(false);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
_tracks[i] = new QCheckBox(QString::number(i + 1));
|
||||
QWidget::connect(_tracks[i], SIGNAL(stateChanged(int)), this,
|
||||
SLOT(TrackChanged()));
|
||||
}
|
||||
|
||||
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
|
||||
SLOT(FilePathChanged(const QString &)));
|
||||
QWidget::connect(
|
||||
_volumeDB,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this, SLOT(VolumeDBChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_monitorTypes, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(MonitorTypeChanged(int)));
|
||||
QWidget::connect(_useStartOffset, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseStartOffsetChanged(int)));
|
||||
QWidget::connect(_useDuration, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseDurationChanged(int)));
|
||||
QWidget::connect(_startOffset,
|
||||
SIGNAL(DurationChanged(const Duration &)), this,
|
||||
SLOT(StartOffsetChanged(const Duration &)));
|
||||
QWidget::connect(_playbackDuration,
|
||||
SIGNAL(DurationChanged(const Duration &)), this,
|
||||
SLOT(PlaybackDurationChanged(const Duration &)));
|
||||
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(WaitChanged(int)));
|
||||
|
||||
auto tracksLayout = new QHBoxLayout;
|
||||
tracksLayout->setContentsMargins(0, 0, 0, 0);
|
||||
tracksLayout->addWidget(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.action.playAudio.tracks")));
|
||||
for (auto *cb : _tracks) {
|
||||
tracksLayout->addWidget(cb);
|
||||
}
|
||||
tracksLayout->addStretch();
|
||||
_tracksContainer->setLayout(tracksLayout);
|
||||
|
||||
auto volumeLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.layout.volume"),
|
||||
volumeLayout, {{"{{volumeDB}}", _volumeDB}});
|
||||
auto monitorLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.layout.monitor"),
|
||||
monitorLayout, {{"{{monitorTypes}}", _monitorTypes}});
|
||||
auto startOffsetLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.layout.startOffset"),
|
||||
startOffsetLayout,
|
||||
{{"{{useStartOffset}}", _useStartOffset},
|
||||
{"{{startOffset}}", _startOffset}});
|
||||
auto playbackDurationLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.playAudio.layout.playbackDuration"),
|
||||
playbackDurationLayout,
|
||||
{{"{{useDuration}}", _useDuration},
|
||||
{"{{playbackDuration}}", _playbackDuration}});
|
||||
|
||||
auto mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(_filePath);
|
||||
mainLayout->addLayout(volumeLayout);
|
||||
mainLayout->addLayout(monitorLayout);
|
||||
mainLayout->addWidget(_tracksContainer);
|
||||
mainLayout->addLayout(startOffsetLayout);
|
||||
mainLayout->addLayout(playbackDurationLayout);
|
||||
mainLayout->addWidget(_waitForCompletion);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_filePath->SetPath(_entryData->_filePath);
|
||||
_volumeDB->SetValue(_entryData->_volumeDB);
|
||||
_monitorTypes->setCurrentIndex(
|
||||
static_cast<int>(_entryData->_monitorType));
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
_tracks[i]->setChecked(_entryData->_audioMixers & (1u << i));
|
||||
}
|
||||
_tracksContainer->setVisible(_entryData->_monitorType !=
|
||||
OBS_MONITORING_TYPE_MONITOR_ONLY);
|
||||
_useStartOffset->setChecked(_entryData->_useStartOffset);
|
||||
_startOffset->SetDuration(_entryData->_startOffset);
|
||||
_startOffset->setEnabled(_entryData->_useStartOffset);
|
||||
_useDuration->setChecked(_entryData->_useDuration);
|
||||
_playbackDuration->SetDuration(_entryData->_playbackDuration);
|
||||
_playbackDuration->setEnabled(_entryData->_useDuration);
|
||||
_waitForCompletion->setChecked(_entryData->_waitForCompletion);
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_filePath = path.toStdString();
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::VolumeDBChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_volumeDB = value;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::MonitorTypeChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_monitorType = static_cast<obs_monitoring_type>(value);
|
||||
_tracksContainer->setVisible(value != OBS_MONITORING_TYPE_MONITOR_ONLY);
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::TrackChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
uint32_t mixers = 0;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (_tracks[i]->isChecked()) {
|
||||
mixers |= (1u << i);
|
||||
}
|
||||
}
|
||||
_entryData->_audioMixers = mixers;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::UseStartOffsetChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_useStartOffset = value;
|
||||
_startOffset->setEnabled(_entryData->_useStartOffset);
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::StartOffsetChanged(const Duration &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_startOffset = value;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::UseDurationChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_useDuration = value;
|
||||
_playbackDuration->setEnabled(_entryData->_useDuration);
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::PlaybackDurationChanged(const Duration &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_playbackDuration = value;
|
||||
}
|
||||
|
||||
void MacroActionPlayAudioEdit::WaitChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_waitForCompletion = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
91
plugins/base/macro-action-play-audio.hpp
Normal file
91
plugins/base/macro-action-play-audio.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "duration-control.hpp"
|
||||
#include "file-selection.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroActionPlayAudio : public MacroAction {
|
||||
public:
|
||||
MacroActionPlayAudio(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; }
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
StringVariable _filePath =
|
||||
obs_module_text("AdvSceneSwitcher.enterPath");
|
||||
DoubleVariable _volumeDB = 0.0;
|
||||
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_MONITOR_ONLY;
|
||||
uint32_t _audioMixers = 0x3F; // tracks 1-6, all on by default
|
||||
bool _useStartOffset = false;
|
||||
Duration _startOffset;
|
||||
bool _useDuration = false;
|
||||
Duration _playbackDuration;
|
||||
bool _waitForCompletion = false;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionPlayAudioEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionPlayAudioEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionPlayAudio> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionPlayAudioEdit(
|
||||
parent, std::dynamic_pointer_cast<MacroActionPlayAudio>(
|
||||
action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void FilePathChanged(const QString &path);
|
||||
void VolumeDBChanged(const NumberVariable<double> &value);
|
||||
void MonitorTypeChanged(int value);
|
||||
void TrackChanged();
|
||||
void UseStartOffsetChanged(int);
|
||||
void StartOffsetChanged(const Duration &);
|
||||
void UseDurationChanged(int);
|
||||
void PlaybackDurationChanged(const Duration &);
|
||||
void WaitChanged(int value);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
private:
|
||||
FileSelection *_filePath;
|
||||
VariableDoubleSpinBox *_volumeDB;
|
||||
QComboBox *_monitorTypes;
|
||||
QWidget *_tracksContainer;
|
||||
std::array<QCheckBox *, 6> _tracks;
|
||||
QCheckBox *_useStartOffset;
|
||||
DurationSelection *_startOffset;
|
||||
QCheckBox *_useDuration;
|
||||
DurationSelection *_playbackDuration;
|
||||
QCheckBox *_waitForCompletion;
|
||||
|
||||
std::shared_ptr<MacroActionPlayAudio> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -47,20 +47,28 @@ getNextMacros(std::vector<MacroRef> ¯os, MacroRef &lastRandomMacroRef,
|
||||
bool MacroActionRandom::PerformAction()
|
||||
{
|
||||
if (_macros.size() == 0) {
|
||||
SetTempVarValue("macro", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat);
|
||||
if (macros.size() == 0) {
|
||||
SetTempVarValue("macro", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macros.size() == 1) {
|
||||
lastRandomMacro = macros[0];
|
||||
SetTempVarValue("macro", GetMacroName(macros[0].get()));
|
||||
return RunMacroActions(macros[0].get());
|
||||
}
|
||||
|
||||
srand((unsigned int)time(0));
|
||||
size_t idx = std::rand() % (macros.size());
|
||||
lastRandomMacro = macros[idx];
|
||||
|
||||
SetTempVarValue("macro", GetMacroName(macros[idx].get()));
|
||||
|
||||
return RunMacroActions(macros[idx].get());
|
||||
}
|
||||
|
||||
@@ -85,6 +93,11 @@ bool MacroActionRandom::Load(obs_data_t *obj)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionRandom::PostLoad()
|
||||
{
|
||||
return MacroAction::PostLoad() && MultiMacroRefAction::PostLoad();
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionRandom>(m);
|
||||
@@ -95,6 +108,16 @@ std::shared_ptr<MacroAction> MacroActionRandom::Copy() const
|
||||
return std::make_shared<MacroActionRandom>(*this);
|
||||
}
|
||||
|
||||
void MacroActionRandom::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
AddTempvar(
|
||||
"macro",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.random.macro"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.random.macro.description"));
|
||||
}
|
||||
|
||||
MacroActionRandomEdit::MacroActionRandomEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
|
||||
: QWidget(parent),
|
||||
|
||||
@@ -15,6 +15,7 @@ public:
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
bool PostLoad();
|
||||
std::string GetId() const { return id; };
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
@@ -22,7 +23,10 @@ public:
|
||||
bool _allowRepeat = false;
|
||||
|
||||
private:
|
||||
void SetupTempVars();
|
||||
|
||||
MacroRef lastRandomMacro;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
@@ -22,12 +22,18 @@ const static std::map<MacroActionRecord::Action, std::string> actionTypes = {
|
||||
"AdvSceneSwitcher.action.recording.type.pause"},
|
||||
{MacroActionRecord::Action::UNPAUSE,
|
||||
"AdvSceneSwitcher.action.recording.type.unpause"},
|
||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(28, 0, 0)
|
||||
{MacroActionRecord::Action::SPLIT,
|
||||
"AdvSceneSwitcher.action.recording.type.split"},
|
||||
#endif
|
||||
{MacroActionRecord::Action::FOLDER,
|
||||
"AdvSceneSwitcher.action.recording.type.changeOutputFolder"},
|
||||
{MacroActionRecord::Action::FILE_FORMAT,
|
||||
"AdvSceneSwitcher.action.recording.type.changeOutputFileFormat"},
|
||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
|
||||
{MacroActionRecord::Action::ADD_CHAPTER,
|
||||
"AdvSceneSwitcher.action.recording.type.addChapter"},
|
||||
#endif
|
||||
};
|
||||
|
||||
bool MacroActionRecord::PerformAction()
|
||||
@@ -87,6 +93,13 @@ bool MacroActionRecord::PerformAction()
|
||||
}
|
||||
break;
|
||||
}
|
||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
|
||||
case Action::ADD_CHAPTER:
|
||||
if (!obs_frontend_recording_add_chapter(_chapterName.c_str())) {
|
||||
blog(LOG_WARNING, "failed to add recoding chapter!");
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -110,6 +123,7 @@ bool MacroActionRecord::Save(obs_data_t *obj) const
|
||||
obs_data_set_int(obj, "action", static_cast<int>(_action));
|
||||
_folder.Save(obj, "folder");
|
||||
_fileFormat.Save(obj, "format");
|
||||
_chapterName.Save(obj, "chapterName");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -119,6 +133,7 @@ bool MacroActionRecord::Load(obs_data_t *obj)
|
||||
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
|
||||
_folder.Load(obj, "folder");
|
||||
_fileFormat.Load(obj, "format");
|
||||
_chapterName.Load(obj, "chapterName");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,6 +151,7 @@ void MacroActionRecord::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_folder.ResolveVariables();
|
||||
_fileFormat.ResolveVariables();
|
||||
_chapterName.ResolveVariables();
|
||||
}
|
||||
|
||||
static inline void populateActionSelection(QComboBox *list)
|
||||
@@ -154,7 +170,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
||||
_splitHint(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.split.hint"))),
|
||||
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
|
||||
_recordFileFormat(new VariableLineEdit(this))
|
||||
_recordFileFormat(new VariableLineEdit(this)),
|
||||
_chapterName(new VariableLineEdit(this))
|
||||
{
|
||||
populateActionSelection(_actions);
|
||||
|
||||
@@ -164,6 +181,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
||||
this, SLOT(FolderChanged(const QString &)));
|
||||
QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this,
|
||||
SLOT(FormatStringChanged()));
|
||||
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
|
||||
SLOT(ChapterNameChanged()));
|
||||
|
||||
auto mainLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
|
||||
@@ -172,7 +191,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
||||
{"{{pauseHint}}", _pauseHint},
|
||||
{"{{splitHint}}", _splitHint},
|
||||
{"{{recordFolder}}", _recordFolder},
|
||||
{"{{recordFileFormat}}", _recordFileFormat}});
|
||||
{"{{recordFileFormat}}", _recordFileFormat},
|
||||
{"{{chapterName}}", _chapterName}});
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
@@ -188,6 +208,7 @@ void MacroActionRecordEdit::UpdateEntryData()
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
||||
_recordFolder->SetPath(_entryData->_folder);
|
||||
_recordFileFormat->setText(_entryData->_fileFormat);
|
||||
_chapterName->setText(_entryData->_chapterName);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -209,6 +230,12 @@ void MacroActionRecordEdit::FormatStringChanged()
|
||||
_entryData->_fileFormat = _recordFileFormat->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroActionRecordEdit::ChapterNameChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_chapterName = _chapterName->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroActionRecordEdit::SetWidgetVisibility()
|
||||
{
|
||||
_pauseHint->setVisible(isPauseAction(_entryData->_action));
|
||||
@@ -218,6 +245,8 @@ void MacroActionRecordEdit::SetWidgetVisibility()
|
||||
MacroActionRecord::Action::FOLDER);
|
||||
_recordFileFormat->setVisible(_entryData->_action ==
|
||||
MacroActionRecord::Action::FILE_FORMAT);
|
||||
_chapterName->setVisible(_entryData->_action ==
|
||||
MacroActionRecord::Action::ADD_CHAPTER);
|
||||
}
|
||||
|
||||
void MacroActionRecordEdit::ActionChanged(int value)
|
||||
|
||||
@@ -29,11 +29,13 @@ public:
|
||||
SPLIT,
|
||||
FOLDER,
|
||||
FILE_FORMAT,
|
||||
ADD_CHAPTER,
|
||||
};
|
||||
Action _action = Action::STOP;
|
||||
|
||||
StringVariable _folder = QDir::homePath().toStdString() + "/Videos";
|
||||
StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss";
|
||||
StringVariable _chapterName = "";
|
||||
|
||||
private:
|
||||
static bool _registered;
|
||||
@@ -60,6 +62,7 @@ private slots:
|
||||
void ActionChanged(int value);
|
||||
void FolderChanged(const QString &);
|
||||
void FormatStringChanged();
|
||||
void ChapterNameChanged();
|
||||
|
||||
protected:
|
||||
QComboBox *_actions;
|
||||
@@ -67,6 +70,7 @@ protected:
|
||||
QLabel *_splitHint;
|
||||
FileSelection *_recordFolder;
|
||||
VariableLineEdit *_recordFileFormat;
|
||||
VariableLineEdit *_chapterName;
|
||||
std::shared_ptr<MacroActionRecord> _entryData;
|
||||
|
||||
private:
|
||||
|
||||
@@ -15,9 +15,20 @@ bool MacroActionRun::_registered = MacroActionFactory::Register(
|
||||
bool MacroActionRun::PerformAction()
|
||||
{
|
||||
if (_wait) {
|
||||
_procConfig.StartProcessAndWait(_timeout.Milliseconds());
|
||||
SetTempVarValues();
|
||||
|
||||
// Snapshot config before releasing the lock for the blocking wait
|
||||
auto procConfig = _procConfig;
|
||||
const auto timeout = _timeout.Milliseconds();
|
||||
{
|
||||
SuspendLock suspendLock(*this);
|
||||
procConfig.StartProcessAndWait(timeout);
|
||||
}
|
||||
SetTempVarValue("process.id", procConfig.GetProcessId());
|
||||
SetTempVarValue("process.exitCode",
|
||||
procConfig.GetProcessExitCode());
|
||||
SetTempVarValue("process.stream.output",
|
||||
procConfig.GetProcessOutputStream());
|
||||
SetTempVarValue("process.stream.error",
|
||||
procConfig.GetProcessErrorStream());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,13 +82,22 @@ void MacroActionSequence::ResolveVariablesToFixedValues()
|
||||
_resetIndex.ResolveVariables();
|
||||
}
|
||||
|
||||
void MacroActionSequence::SetAction(Action action)
|
||||
{
|
||||
_action = action;
|
||||
SetupTempVars();
|
||||
}
|
||||
|
||||
bool MacroActionSequence::RunSequence()
|
||||
{
|
||||
if (_macros.size() == 0) {
|
||||
SetTempVarValue("macro", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
auto macro = GetNextMacro().GetMacro();
|
||||
SetTempVarValue("macro", GetMacroName(macro.get()));
|
||||
|
||||
if (!macro.get()) {
|
||||
return true;
|
||||
}
|
||||
@@ -96,18 +105,21 @@ bool MacroActionSequence::RunSequence()
|
||||
return RunMacroActions(macro.get());
|
||||
}
|
||||
|
||||
bool MacroActionSequence::SetSequenceIndex() const
|
||||
bool MacroActionSequence::SetSequenceIndex()
|
||||
{
|
||||
auto macro = _macro.GetMacro();
|
||||
if (!macro) {
|
||||
SetTempVarValue("nextMacro", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
auto actions = GetMacroActions(macro.get());
|
||||
if (!actions) {
|
||||
SetTempVarValue("nextMacro", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string nextMacroName;
|
||||
for (const auto &action : *actions) {
|
||||
if (action->GetId() != id) {
|
||||
continue;
|
||||
@@ -121,10 +133,34 @@ bool MacroActionSequence::SetSequenceIndex() const
|
||||
// -2 is needed since the _lastIndex starts at -1 and the reset
|
||||
// index starts at 1
|
||||
sequenceAction->_lastIdx = _resetIndex - 2;
|
||||
nextMacroName = GetMacroName(
|
||||
sequenceAction->GetNextMacro(false).GetMacro().get());
|
||||
}
|
||||
SetTempVarValue("nextMacro", nextMacroName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionSequence::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
|
||||
if (_action == Action::RUN_SEQUENCE) {
|
||||
AddTempvar(
|
||||
"macro",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.sequence.macro"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.sequence.macro.description"));
|
||||
} else if (_action == Action::SET_INDEX) {
|
||||
AddTempvar(
|
||||
"nextMacro",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.sequence.nextMacro"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.sequence.nextMacro.description"));
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionSequence::PerformAction()
|
||||
{
|
||||
if (_action == Action::RUN_SEQUENCE) {
|
||||
@@ -162,7 +198,7 @@ bool MacroActionSequence::Load(obs_data_t *obj)
|
||||
LoadMacroList(obj, _macros);
|
||||
_restart = obs_data_get_bool(obj, "restart");
|
||||
_macro.Load(obj);
|
||||
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
|
||||
SetAction(static_cast<Action>(obs_data_get_int(obj, "action")));
|
||||
_resetIndex.Load(obj, "resetIndex");
|
||||
return true;
|
||||
}
|
||||
@@ -256,7 +292,7 @@ void MacroActionSequenceEdit::UpdateEntryData()
|
||||
_macroList->SetContent(_entryData->_macros);
|
||||
_restart->setChecked(_entryData->_restart);
|
||||
_resetIndex->SetValue(_entryData->_resetIndex);
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
|
||||
_macros->SetCurrentMacro(_entryData->_macro);
|
||||
SetWidgetVisibility();
|
||||
adjustSize();
|
||||
@@ -361,7 +397,7 @@ void MacroActionSequenceEdit::UpdateStatusLine()
|
||||
void MacroActionSequenceEdit::ActionChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_action = static_cast<MacroActionSequence::Action>(value);
|
||||
_entryData->SetAction(static_cast<MacroActionSequence::Action>(value));
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -385,10 +421,10 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
|
||||
|
||||
ClearLayout(_layout);
|
||||
|
||||
const auto action = _entryData->GetAction();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
_entryData->_action ==
|
||||
MacroActionSequence::Action::RUN_SEQUENCE
|
||||
action == MacroActionSequence::Action::RUN_SEQUENCE
|
||||
? "AdvSceneSwitcher.action.sequence.entry.run"
|
||||
: "AdvSceneSwitcher.action.sequence.entry.setIndex"),
|
||||
_layout,
|
||||
@@ -396,15 +432,14 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
|
||||
{"{{macros}}", _macros},
|
||||
{"{{index}}", _resetIndex}});
|
||||
|
||||
_macroList->setVisible(_entryData->_action ==
|
||||
_macroList->setVisible(action ==
|
||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||
_restart->setVisible(_entryData->_action ==
|
||||
_restart->setVisible(action ==
|
||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||
_statusLine->setVisible(_entryData->_action ==
|
||||
_statusLine->setVisible(action ==
|
||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||
_macros->setVisible(_entryData->_action ==
|
||||
MacroActionSequence::Action::SET_INDEX);
|
||||
_resetIndex->setVisible(_entryData->_action ==
|
||||
_macros->setVisible(action == MacroActionSequence::Action::SET_INDEX);
|
||||
_resetIndex->setVisible(action ==
|
||||
MacroActionSequence::Action::SET_INDEX);
|
||||
|
||||
adjustSize();
|
||||
|
||||
@@ -34,7 +34,10 @@ public:
|
||||
RUN_SEQUENCE,
|
||||
SET_INDEX,
|
||||
};
|
||||
Action _action = Action::RUN_SEQUENCE;
|
||||
|
||||
Action GetAction() const { return _action; }
|
||||
void SetAction(Action);
|
||||
|
||||
bool _restart = true;
|
||||
IntVariable _resetIndex = 1;
|
||||
|
||||
@@ -43,7 +46,11 @@ public:
|
||||
|
||||
private:
|
||||
bool RunSequence();
|
||||
bool SetSequenceIndex() const;
|
||||
bool SetSequenceIndex();
|
||||
|
||||
void SetupTempVars();
|
||||
|
||||
Action _action = Action::RUN_SEQUENCE;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
|
||||
292
plugins/base/macro-action-source-interaction.cpp
Normal file
292
plugins/base/macro-action-source-interaction.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
#include "macro-action-source-interaction.hpp"
|
||||
#include "source-interaction-recorder.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <obs-interaction.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroActionSourceInteraction::id = "source_interaction";
|
||||
|
||||
bool MacroActionSourceInteraction::_registered = MacroActionFactory::Register(
|
||||
MacroActionSourceInteraction::id,
|
||||
{MacroActionSourceInteraction::Create,
|
||||
MacroActionSourceInteractionEdit::Create,
|
||||
"AdvSceneSwitcher.action.sourceInteraction"});
|
||||
|
||||
bool MacroActionSourceInteraction::PerformAction()
|
||||
{
|
||||
OBSSourceAutoRelease source =
|
||||
obs_weak_source_get_source(_source.GetSource());
|
||||
|
||||
if (!source) {
|
||||
blog(LOG_WARNING, "source interaction: source not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t flags = obs_source_get_output_flags(source);
|
||||
if (!(flags & OBS_SOURCE_INTERACTION)) {
|
||||
blog(LOG_WARNING,
|
||||
"source interaction: source \"%s\" does not support interaction",
|
||||
obs_source_get_name(source));
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto &step : _steps) {
|
||||
PerformInteractionStep(source, step);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionSourceInteraction::LogAction() const
|
||||
{
|
||||
ablog(LOG_INFO, "performed source interaction on \"%s\" (%d steps)",
|
||||
_source.ToString(true).c_str(), (int)_steps.size());
|
||||
}
|
||||
|
||||
bool MacroActionSourceInteraction::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
_source.Save(obj);
|
||||
|
||||
OBSDataArrayAutoRelease arr = obs_data_array_create();
|
||||
for (const auto &step : _steps) {
|
||||
OBSDataAutoRelease stepObj = obs_data_create();
|
||||
step.Save(stepObj);
|
||||
obs_data_array_push_back(arr, stepObj);
|
||||
}
|
||||
obs_data_set_array(obj, "steps", arr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionSourceInteraction::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_source.Load(obj);
|
||||
|
||||
_steps.clear();
|
||||
OBSDataArrayAutoRelease arr = obs_data_get_array(obj, "steps");
|
||||
size_t count = obs_data_array_count(arr);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
OBSDataAutoRelease stepObj = obs_data_array_item(arr, i);
|
||||
SourceInteractionStep step;
|
||||
step.Load(stepObj);
|
||||
_steps.push_back(step);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroActionSourceInteraction::GetShortDesc() const
|
||||
{
|
||||
return _source.ToString();
|
||||
}
|
||||
|
||||
void MacroActionSourceInteraction::ResolveVariablesToFixedValues()
|
||||
{
|
||||
for (auto &step : _steps) {
|
||||
step.x.ResolveVariables();
|
||||
step.y.ResolveVariables();
|
||||
step.clickCount.ResolveVariables();
|
||||
step.wheelDeltaX.ResolveVariables();
|
||||
step.wheelDeltaY.ResolveVariables();
|
||||
step.nativeVkey.ResolveVariables();
|
||||
step.text.ResolveVariables();
|
||||
step.waitMs.ResolveVariables();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionSourceInteraction>(m);
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Copy() const
|
||||
{
|
||||
return std::make_shared<MacroActionSourceInteraction>(*this);
|
||||
}
|
||||
|
||||
static QStringList getInteractableSourceNames()
|
||||
{
|
||||
QStringList names;
|
||||
obs_enum_sources(
|
||||
[](void *param, obs_source_t *source) -> bool {
|
||||
uint32_t flags = obs_source_get_output_flags(source);
|
||||
if (flags & OBS_SOURCE_INTERACTION) {
|
||||
auto list = static_cast<QStringList *>(param);
|
||||
const char *name = obs_source_get_name(source);
|
||||
if (name) {
|
||||
list->append(QString(name));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
&names);
|
||||
names.sort();
|
||||
return names;
|
||||
}
|
||||
|
||||
MacroActionSourceInteractionEdit::MacroActionSourceInteractionEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionSourceInteraction> entryData)
|
||||
: QWidget(parent),
|
||||
_sources(new SourceSelectionWidget(this, getInteractableSourceNames,
|
||||
true)),
|
||||
_stepList(new SourceInteractionStepList(this)),
|
||||
_recordButton(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record"))),
|
||||
_noSelectionLabel(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.noSelection")))
|
||||
{
|
||||
_stepList->AddControlWidget(_recordButton);
|
||||
|
||||
auto sourceRow = new QHBoxLayout;
|
||||
sourceRow->addWidget(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.source")));
|
||||
sourceRow->addWidget(_sources);
|
||||
sourceRow->addStretch();
|
||||
|
||||
_noSelectionLabel->setAlignment(Qt::AlignCenter);
|
||||
_noSelectionLabel->hide();
|
||||
|
||||
auto mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addLayout(sourceRow);
|
||||
mainLayout->addWidget(_stepList);
|
||||
mainLayout->addWidget(_noSelectionLabel);
|
||||
setLayout(mainLayout);
|
||||
|
||||
connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
connect(_stepList, &SourceInteractionStepList::StepsChanged, this,
|
||||
&MacroActionSourceInteractionEdit::OnStepsChanged);
|
||||
connect(_stepList, &SourceInteractionStepList::RowSelected, this,
|
||||
&MacroActionSourceInteractionEdit::SetCurrentStepEditor);
|
||||
connect(_recordButton, &QPushButton::clicked, this,
|
||||
&MacroActionSourceInteractionEdit::OpenRecorder);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_sources->SetSource(_entryData->_source);
|
||||
_stepList->SetSteps(_entryData->_steps);
|
||||
_noSelectionLabel->setVisible(_stepList->count() > 0);
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::SourceChanged(
|
||||
const SourceSelection &source)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_source = source;
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::OnStepsChanged(
|
||||
const std::vector<SourceInteractionStep> &steps)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_steps = steps;
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::StepChanged(
|
||||
const SourceInteractionStep &step)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
int row = _stepList->CurrentRow();
|
||||
if (row < 0 || row >= (int)_entryData->_steps.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_entryData->_steps[row] = step;
|
||||
_stepList->UpdateStep(row, step);
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::OpenRecorder()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSSourceAutoRelease source =
|
||||
obs_weak_source_get_source(_entryData->_source.GetSource());
|
||||
if (!source) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.invalidSource"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dlg = new SourceInteractionRecorder(
|
||||
window(), _entryData->_source.GetSource());
|
||||
connect(dlg, &SourceInteractionRecorder::StepsRecorded, this,
|
||||
&MacroActionSourceInteractionEdit::AcceptRecorded);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->show();
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::AcceptRecorded(
|
||||
const std::vector<SourceInteractionStep> &steps)
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
auto lock = _entryData->Lock();
|
||||
_entryData->_steps.insert(_entryData->_steps.end(),
|
||||
steps.begin(), steps.end());
|
||||
}
|
||||
_stepList->SetSteps(_entryData->_steps);
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
|
||||
{
|
||||
const QSignalBlocker b(this);
|
||||
UpdateEntryData();
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::SetCurrentStepEditor(int row)
|
||||
{
|
||||
if (_stepEditor) {
|
||||
delete _stepEditor;
|
||||
_stepEditor = nullptr;
|
||||
}
|
||||
|
||||
if (!_entryData || row < 0 || row >= (int)_entryData->_steps.size()) {
|
||||
_noSelectionLabel->setVisible(_stepList->count() > 0);
|
||||
return;
|
||||
}
|
||||
|
||||
_noSelectionLabel->setVisible(false);
|
||||
|
||||
_stepEditor =
|
||||
new SourceInteractionStepEdit(this, _entryData->_steps[row]);
|
||||
static_cast<QVBoxLayout *>(layout())->addWidget(_stepEditor);
|
||||
connect(_stepEditor, &SourceInteractionStepEdit::StepChanged, this,
|
||||
&MacroActionSourceInteractionEdit::StepChanged);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
81
plugins/base/macro-action-source-interaction.hpp
Normal file
81
plugins/base/macro-action-source-interaction.hpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "source-selection.hpp"
|
||||
#include "source-interaction-step.hpp"
|
||||
#include "source-interaction-step-list.hpp"
|
||||
#include "source-interaction-step-edit.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroActionSourceInteraction : public MacroAction {
|
||||
public:
|
||||
MacroActionSourceInteraction(Macro *m) : MacroAction(m) {}
|
||||
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
void ResolveVariablesToFixedValues() override;
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
|
||||
std::string GetId() const { return id; }
|
||||
std::string GetShortDesc() const;
|
||||
|
||||
SourceSelection _source;
|
||||
std::vector<SourceInteractionStep> _steps;
|
||||
|
||||
private:
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionSourceInteractionEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MacroActionSourceInteractionEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionSourceInteraction> entryData =
|
||||
nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionSourceInteractionEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroActionSourceInteraction>(
|
||||
action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void OnStepsChanged(const std::vector<SourceInteractionStep> &);
|
||||
void StepChanged(const SourceInteractionStep &);
|
||||
void OpenRecorder();
|
||||
void AcceptRecorded(const std::vector<SourceInteractionStep> &);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
void SetCurrentStepEditor(int row);
|
||||
|
||||
SourceSelectionWidget *_sources;
|
||||
SourceInteractionStepList *_stepList;
|
||||
QPushButton *_recordButton;
|
||||
SourceInteractionStepEdit *_stepEditor = nullptr;
|
||||
QLabel *_noSelectionLabel;
|
||||
|
||||
std::shared_ptr<MacroActionSourceInteraction> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -13,6 +13,14 @@ namespace advss {
|
||||
|
||||
const std::string MacroActionSource::id = "source";
|
||||
|
||||
std::vector<TempVariableRef> MacroActionSource::GetTempVarRefs() const
|
||||
{
|
||||
if (!_tempVar.HasValidID()) {
|
||||
return {};
|
||||
}
|
||||
return {_tempVar};
|
||||
}
|
||||
|
||||
bool MacroActionSource::_registered = MacroActionFactory::Register(
|
||||
MacroActionSource::id,
|
||||
{MacroActionSource::Create, MacroActionSourceEdit::Create,
|
||||
@@ -45,6 +53,10 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
|
||||
"AdvSceneSwitcher.action.source.type.closeFilterDialog"},
|
||||
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
|
||||
"AdvSceneSwitcher.action.source.type.closePropertiesDialog"},
|
||||
{MacroActionSource::Action::GET_SETTING,
|
||||
"AdvSceneSwitcher.action.source.type.getSetting"},
|
||||
{MacroActionSource::Action::GET_SETTINGS,
|
||||
"AdvSceneSwitcher.action.source.type.getSettings"},
|
||||
};
|
||||
|
||||
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
|
||||
@@ -273,6 +285,22 @@ bool MacroActionSource::PerformAction()
|
||||
"OBSBasicProperties");
|
||||
});
|
||||
break;
|
||||
case Action::GET_SETTING: {
|
||||
const auto value =
|
||||
GetSourceSettingValue(_source.GetSource(), _setting);
|
||||
if (value) {
|
||||
SetTempVarValue("setting", *value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Action::GET_SETTINGS: {
|
||||
const auto settings =
|
||||
GetSourceSettings(_source.GetSource(), true);
|
||||
if (settings) {
|
||||
SetTempVarValue("settings", *settings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -360,6 +388,31 @@ void MacroActionSource::ResolveVariablesToFixedValues()
|
||||
_manualSettingValue.ResolveVariables();
|
||||
}
|
||||
|
||||
void MacroActionSource::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
switch (_action) {
|
||||
case Action::GET_SETTING:
|
||||
AddTempvar("setting",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.source.setting"));
|
||||
break;
|
||||
case Action::GET_SETTINGS:
|
||||
AddTempvar("settings",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.source.settings"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionSource::SetAction(Action action)
|
||||
{
|
||||
_action = action;
|
||||
SetupTempVars();
|
||||
}
|
||||
|
||||
static inline void populateActionSelection(QComboBox *list)
|
||||
{
|
||||
for (auto &[actionType, name] : actionTypes) {
|
||||
@@ -502,7 +555,7 @@ void MacroActionSourceEdit::UpdateEntryData()
|
||||
|
||||
const auto weakSource = _entryData->_source.GetSource();
|
||||
_settingsButtons->SetSelection(weakSource, _entryData->_button);
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
|
||||
_sources->SetSource(_entryData->_source);
|
||||
_sourceSettings->SetSelection(weakSource, _entryData->_setting);
|
||||
_settingsString->setPlainText(_entryData->_settingsString);
|
||||
@@ -537,7 +590,7 @@ void MacroActionSourceEdit::SourceChanged(const SourceSelection &source)
|
||||
void MacroActionSourceEdit::ActionChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_action = static_cast<MacroActionSource::Action>(value);
|
||||
_entryData->SetAction(static_cast<MacroActionSource::Action>(value));
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -607,6 +660,7 @@ void MacroActionSourceEdit::SelectionChanged(const TempVariableRef &var)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_tempVar = var;
|
||||
IncrementTempVarInUseGeneration();
|
||||
}
|
||||
|
||||
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
|
||||
@@ -664,19 +718,29 @@ static QString GetIndividualListEntryName()
|
||||
|
||||
void MacroActionSourceEdit::SetWidgetVisibility()
|
||||
{
|
||||
const auto action = _entryData->GetAction();
|
||||
|
||||
const bool isSetSettings = action ==
|
||||
MacroActionSource::Action::SETTINGS;
|
||||
const bool isGetSetting = action ==
|
||||
MacroActionSource::Action::GET_SETTING;
|
||||
const bool isGetSettings = action ==
|
||||
MacroActionSource::Action::GET_SETTINGS;
|
||||
|
||||
SetLayoutVisible(_settingsLayout,
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::SETTINGS);
|
||||
isSetSettings || isGetSetting || isGetSettings);
|
||||
_settingsInputMethods->setVisible(isSetSettings);
|
||||
_sourceSettings->setVisible(
|
||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
||||
_entryData->_settingsInputMethod !=
|
||||
MacroActionSource::SettingsInputMethod::JSON_STRING);
|
||||
(isSetSettings &&
|
||||
_entryData->_settingsInputMethod !=
|
||||
MacroActionSource::SettingsInputMethod::JSON_STRING) ||
|
||||
isGetSetting);
|
||||
_settingsString->setVisible(
|
||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
||||
isSetSettings &&
|
||||
_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::JSON_STRING);
|
||||
_getSettings->setVisible(
|
||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
||||
isSetSettings &&
|
||||
_entryData->_settingsInputMethod !=
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_TEMPVAR);
|
||||
@@ -685,13 +749,12 @@ void MacroActionSourceEdit::SetWidgetVisibility()
|
||||
GetIndividualListEntryName(),
|
||||
_entryData->_setting.IsList());
|
||||
|
||||
_tempVars->setVisible(_entryData->_action ==
|
||||
MacroActionSource::Action::SETTINGS &&
|
||||
_tempVars->setVisible(isSetSettings &&
|
||||
_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_TEMPVAR);
|
||||
|
||||
if (_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
||||
if (isSetSettings &&
|
||||
(_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL ||
|
||||
_entryData->_settingsInputMethod ==
|
||||
@@ -704,35 +767,31 @@ void MacroActionSourceEdit::SetWidgetVisibility()
|
||||
_manualSettingValue->hide();
|
||||
}
|
||||
|
||||
const bool showWarning =
|
||||
_entryData->_action == MacroActionSource::Action::ENABLE ||
|
||||
_entryData->_action == MacroActionSource::Action::DISABLE;
|
||||
const bool showWarning = action == MacroActionSource::Action::ENABLE ||
|
||||
action == MacroActionSource::Action::DISABLE;
|
||||
_warning->setVisible(showWarning);
|
||||
_settingsButtons->setVisible(
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::SETTINGS_BUTTON);
|
||||
action == MacroActionSource::Action::SETTINGS_BUTTON);
|
||||
_deinterlaceMode->setVisible(
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::DEINTERLACE_MODE);
|
||||
action == MacroActionSource::Action::DEINTERLACE_MODE);
|
||||
_deinterlaceOrder->setVisible(
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
|
||||
action == MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
|
||||
|
||||
_refreshSettingSelection->setVisible(
|
||||
(_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_MANUAL ||
|
||||
_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_LIST_ENTRY) &&
|
||||
((isSetSettings &&
|
||||
(_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_MANUAL ||
|
||||
_entryData->_settingsInputMethod ==
|
||||
MacroActionSource::SettingsInputMethod::
|
||||
INDIVIDUAL_LIST_ENTRY)) ||
|
||||
isGetSetting) &&
|
||||
_entryData->_source.GetType() ==
|
||||
SourceSelection::Type::VARIABLE);
|
||||
|
||||
_acceptDialog->setVisible(
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
|
||||
_entryData->_action ==
|
||||
MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
|
||||
action == MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
|
||||
action == MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
@@ -15,26 +15,22 @@ namespace advss {
|
||||
class MacroActionSource : public MacroAction {
|
||||
public:
|
||||
MacroActionSource(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; };
|
||||
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
SourceSelection _source;
|
||||
SourceSettingButton _button;
|
||||
StringVariable _settingsString = "";
|
||||
StringVariable _manualSettingValue = "";
|
||||
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
|
||||
obs_deinterlace_field_order _deinterlaceOrder =
|
||||
OBS_DEINTERLACE_FIELD_ORDER_TOP;
|
||||
TempVariableRef _tempVar;
|
||||
SourceSetting _setting;
|
||||
bool _acceptDialog = false;
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; };
|
||||
|
||||
void ResolveVariablesToFixedValues();
|
||||
void SetupTempVars();
|
||||
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||
|
||||
enum class Action {
|
||||
ENABLE,
|
||||
@@ -50,8 +46,23 @@ public:
|
||||
CLOSE_INTERACTION_DIALOG,
|
||||
CLOSE_FILTER_DIALOG,
|
||||
CLOSE_PROPERTIES_DIALOG,
|
||||
GET_SETTING,
|
||||
GET_SETTINGS,
|
||||
};
|
||||
Action _action = Action::SETTINGS;
|
||||
|
||||
void SetAction(Action);
|
||||
Action GetAction() const { return _action; }
|
||||
|
||||
SourceSelection _source;
|
||||
SourceSettingButton _button;
|
||||
StringVariable _settingsString = "";
|
||||
StringVariable _manualSettingValue = "";
|
||||
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
|
||||
obs_deinterlace_field_order _deinterlaceOrder =
|
||||
OBS_DEINTERLACE_FIELD_ORDER_TOP;
|
||||
TempVariableRef _tempVar;
|
||||
SourceSetting _setting;
|
||||
bool _acceptDialog = false;
|
||||
|
||||
enum class SettingsInputMethod {
|
||||
INDIVIDUAL_MANUAL,
|
||||
@@ -63,6 +74,8 @@ public:
|
||||
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
||||
|
||||
private:
|
||||
Action _action = Action::SETTINGS;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "transition-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -22,6 +23,10 @@ const static std::map<MacroActionTransition::Type, std::string> actionTypes = {
|
||||
"AdvSceneSwitcher.action.transition.type.sourceShow"},
|
||||
{MacroActionTransition::Type::SOURCE_HIDE,
|
||||
"AdvSceneSwitcher.action.transition.type.sourceHide"},
|
||||
{MacroActionTransition::Type::TBAR,
|
||||
"AdvSceneSwitcher.action.transition.type.tbar"},
|
||||
{MacroActionTransition::Type::RELEASE_TBAR,
|
||||
"AdvSceneSwitcher.action.transition.type.releaseTbar"},
|
||||
};
|
||||
|
||||
void MacroActionTransition::SetSceneTransition()
|
||||
@@ -76,6 +81,13 @@ static void obs_sceneitem_set_transition_duration(obs_sceneitem_t *item,
|
||||
}
|
||||
#endif
|
||||
|
||||
void MacroActionTransition::SetTbarPosition()
|
||||
{
|
||||
const double percent = std::clamp(_tbarPosition.GetValue(), 0.0, 100.0);
|
||||
const int tbarValue = (int)std::round(percent / 100.0 * 1023.0);
|
||||
obs_frontend_set_tbar_position(tbarValue);
|
||||
}
|
||||
|
||||
void MacroActionTransition::SetSourceTransition(bool show)
|
||||
{
|
||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0)
|
||||
@@ -110,6 +122,12 @@ bool MacroActionTransition::PerformAction()
|
||||
case Type::SOURCE_HIDE:
|
||||
SetSourceTransition(false);
|
||||
break;
|
||||
case Type::TBAR:
|
||||
SetTbarPosition();
|
||||
break;
|
||||
case Type::RELEASE_TBAR:
|
||||
obs_frontend_release_tbar();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -119,22 +137,29 @@ void MacroActionTransition::LogAction() const
|
||||
std::string msgBegin;
|
||||
switch (_type) {
|
||||
case Type::SCENE:
|
||||
msgBegin += "set scene transition";
|
||||
msgBegin = "set scene transition";
|
||||
break;
|
||||
case Type::SCENE_OVERRIDE:
|
||||
msgBegin += "set scene override transition of " +
|
||||
_scene.ToString(true);
|
||||
msgBegin = "set scene override transition of " +
|
||||
_scene.ToString(true);
|
||||
break;
|
||||
case Type::SOURCE_SHOW:
|
||||
msgBegin += "set source show transition of " +
|
||||
_source.ToString(true) + " on scene " +
|
||||
_scene.ToString(true);
|
||||
msgBegin = "set source show transition of " +
|
||||
_source.ToString(true) + " on scene " +
|
||||
_scene.ToString(true);
|
||||
break;
|
||||
case Type::SOURCE_HIDE:
|
||||
msgBegin += "set source hide transition of " +
|
||||
_source.ToString(true) + " on scene " +
|
||||
_scene.ToString(true);
|
||||
msgBegin = "set source hide transition of " +
|
||||
_source.ToString(true) + " on scene " +
|
||||
_scene.ToString(true);
|
||||
break;
|
||||
case Type::TBAR:
|
||||
ablog(LOG_INFO, "set T-Bar position to %.2f%%",
|
||||
_tbarPosition.GetValue());
|
||||
return;
|
||||
case Type::RELEASE_TBAR:
|
||||
ablog(LOG_INFO, "release T-Bar");
|
||||
return;
|
||||
}
|
||||
if (_setDuration) {
|
||||
ablog(LOG_INFO, "%s duration to %s", msgBegin.c_str(),
|
||||
@@ -156,6 +181,7 @@ bool MacroActionTransition::Save(obs_data_t *obj) const
|
||||
_transition.Save(obj);
|
||||
obs_data_set_bool(obj, "setDuration", _setDuration);
|
||||
obs_data_set_bool(obj, "setType", _setTransitionType);
|
||||
_tbarPosition.Save(obj, "tbarPosition");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -169,23 +195,25 @@ bool MacroActionTransition::Load(obs_data_t *obj)
|
||||
_transition.Load(obj);
|
||||
_setDuration = obs_data_get_bool(obj, "setDuration");
|
||||
_setTransitionType = obs_data_get_bool(obj, "setType");
|
||||
_tbarPosition.Load(obj, "tbarPosition");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroActionTransition::GetShortDesc() const
|
||||
{
|
||||
std::string msgBegin;
|
||||
switch (_type) {
|
||||
case Type::SCENE:
|
||||
return _transition.ToString();
|
||||
case Type::SCENE_OVERRIDE:
|
||||
return _scene.ToString() + " - " + _transition.ToString();
|
||||
case Type::SOURCE_SHOW:
|
||||
return _scene.ToString() + " - " + _source.ToString() + " - " +
|
||||
_transition.ToString();
|
||||
case Type::SOURCE_HIDE:
|
||||
return _scene.ToString() + " - " + _source.ToString() + " - " +
|
||||
_transition.ToString();
|
||||
case Type::TBAR:
|
||||
return std::to_string(_tbarPosition.GetValue()) + "%";
|
||||
case Type::RELEASE_TBAR:
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -205,6 +233,7 @@ void MacroActionTransition::ResolveVariablesToFixedValues()
|
||||
_source.ResolveVariables();
|
||||
_scene.ResolveVariables();
|
||||
_duration.ResolveVariables();
|
||||
_tbarPosition.ResolveVariables();
|
||||
}
|
||||
|
||||
static inline void populateActionSelection(QComboBox *list)
|
||||
@@ -236,9 +265,16 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
||||
_setDuration(new QCheckBox),
|
||||
_transitions(new TransitionSelectionWidget(this, false)),
|
||||
_duration(new DurationSelection(this, false)),
|
||||
_tbarPosition(new VariableDoubleSpinBox),
|
||||
_transitionLayout(new QHBoxLayout),
|
||||
_durationLayout(new QHBoxLayout)
|
||||
_durationLayout(new QHBoxLayout),
|
||||
_tbarLayout(new QHBoxLayout)
|
||||
{
|
||||
_tbarPosition->setMinimum(0.0);
|
||||
_tbarPosition->setMaximum(100.0);
|
||||
_tbarPosition->setDecimals(2);
|
||||
_tbarPosition->setSuffix("%");
|
||||
|
||||
populateActionSelection(_actions);
|
||||
|
||||
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
|
||||
@@ -260,6 +296,11 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
||||
SLOT(SetTransitionChanged(int)));
|
||||
QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(SetDurationChanged(int)));
|
||||
QWidget::connect(
|
||||
_tbarPosition,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(TbarPositionChanged(const NumberVariable<double> &)));
|
||||
|
||||
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{type}}", _actions},
|
||||
@@ -269,22 +310,29 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
||||
{"{{duration}}", _duration},
|
||||
{"{{setTransition}}", _setTransition},
|
||||
{"{{setDuration}}", _setDuration},
|
||||
{"{{tbarPosition}}", _tbarPosition},
|
||||
};
|
||||
|
||||
auto typeLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.transition.entry.line1"),
|
||||
"AdvSceneSwitcher.action.transition.layout.type"),
|
||||
typeLayout, widgetPlaceholders);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.transition.layout.transition"),
|
||||
_transitionLayout, widgetPlaceholders);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.transition.layout.duration"),
|
||||
_durationLayout, widgetPlaceholders);
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.transition.entry.line2"),
|
||||
_transitionLayout, widgetPlaceholders);
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.transition.entry.line3"),
|
||||
_durationLayout, widgetPlaceholders);
|
||||
"AdvSceneSwitcher.action.transition.layout.tbar"),
|
||||
_tbarLayout, widgetPlaceholders);
|
||||
auto mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(typeLayout);
|
||||
mainLayout->addLayout(_transitionLayout);
|
||||
mainLayout->addLayout(_durationLayout);
|
||||
mainLayout->addLayout(_tbarLayout);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
@@ -300,13 +348,14 @@ void MacroActionTransitionEdit::UpdateEntryData()
|
||||
|
||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_type));
|
||||
_scenes->SetScene(_entryData->_scene);
|
||||
_sources->SetSceneItem((_entryData->_source));
|
||||
_sources->SetSceneItem(_entryData->_source);
|
||||
_setDuration->setChecked(_entryData->_setDuration);
|
||||
_duration->SetDuration(_entryData->_duration);
|
||||
_setTransition->setChecked(_entryData->_setTransitionType);
|
||||
_transitions->SetTransition(_entryData->_transition);
|
||||
_transitions->setEnabled(_entryData->_setTransitionType);
|
||||
_duration->setEnabled(_entryData->_setDuration);
|
||||
_tbarPosition->SetValue(_entryData->_tbarPosition);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
@@ -353,11 +402,19 @@ void MacroActionTransitionEdit::DurationChanged(const Duration &dur)
|
||||
|
||||
void MacroActionTransitionEdit::SetWidgetVisibility()
|
||||
{
|
||||
const bool isTbar = _entryData->_type ==
|
||||
MacroActionTransition::Type::TBAR;
|
||||
const bool isReleaseTbar = _entryData->_type ==
|
||||
MacroActionTransition::Type::RELEASE_TBAR;
|
||||
_sources->setVisible(
|
||||
_entryData->_type == MacroActionTransition::Type::SOURCE_HIDE ||
|
||||
_entryData->_type == MacroActionTransition::Type::SOURCE_SHOW);
|
||||
_scenes->setVisible(_entryData->_type !=
|
||||
MacroActionTransition::Type::SCENE);
|
||||
MacroActionTransition::Type::SCENE &&
|
||||
!isTbar && !isReleaseTbar);
|
||||
SetLayoutVisible(_transitionLayout, !isTbar && !isReleaseTbar);
|
||||
SetLayoutVisible(_durationLayout, !isTbar && !isReleaseTbar);
|
||||
SetLayoutVisible(_tbarLayout, isTbar);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
@@ -381,4 +438,13 @@ void MacroActionTransitionEdit::SetDurationChanged(int state)
|
||||
_duration->setEnabled(state);
|
||||
}
|
||||
|
||||
void MacroActionTransitionEdit::TbarPositionChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_tbarPosition = value;
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "transition-selection.hpp"
|
||||
#include "scene-selection.hpp"
|
||||
#include "scene-item-selection.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
@@ -28,6 +29,8 @@ public:
|
||||
SCENE_OVERRIDE,
|
||||
SOURCE_SHOW,
|
||||
SOURCE_HIDE,
|
||||
TBAR,
|
||||
RELEASE_TBAR,
|
||||
};
|
||||
|
||||
Type _type = Type::SCENE;
|
||||
@@ -37,11 +40,13 @@ public:
|
||||
bool _setTransitionType = true;
|
||||
TransitionSelection _transition;
|
||||
Duration _duration;
|
||||
DoubleVariable _tbarPosition = 0.0;
|
||||
|
||||
private:
|
||||
void SetSceneTransition();
|
||||
void SetTransitionOverride();
|
||||
void SetSourceTransition(bool);
|
||||
void SetTbarPosition();
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
@@ -72,6 +77,7 @@ private slots:
|
||||
void SetDurationChanged(int state);
|
||||
void TransitionChanged(const TransitionSelection &);
|
||||
void DurationChanged(const Duration &seconds);
|
||||
void TbarPositionChanged(const NumberVariable<double> &);
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
@@ -83,8 +89,10 @@ protected:
|
||||
QCheckBox *_setDuration;
|
||||
TransitionSelectionWidget *_transitions;
|
||||
DurationSelection *_duration;
|
||||
VariableDoubleSpinBox *_tbarPosition;
|
||||
QHBoxLayout *_transitionLayout;
|
||||
QHBoxLayout *_durationLayout;
|
||||
QHBoxLayout *_tbarLayout;
|
||||
std::shared_ptr<MacroActionTransition> _entryData;
|
||||
|
||||
private:
|
||||
|
||||
@@ -57,8 +57,11 @@ bool MacroActionWait::PerformAction()
|
||||
std::chrono::milliseconds((int)(sleepDuration * 1000));
|
||||
|
||||
SetMacroAbortWait(false);
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
waitHelper(&lock, GetMacro(), time);
|
||||
{
|
||||
SuspendLock suspendLock(*this);
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
waitHelper(&lock, GetMacro(), time);
|
||||
}
|
||||
|
||||
return !MacroWaitShouldAbort();
|
||||
}
|
||||
|
||||
@@ -710,6 +710,12 @@ void MacroConditionDateEdit::UpdateEntryData()
|
||||
SetWidgetStatus();
|
||||
}
|
||||
|
||||
void MacroConditionDateEdit::showEvent(QShowEvent *event)
|
||||
{
|
||||
const QSignalBlocker b(this);
|
||||
UpdateEntryData();
|
||||
}
|
||||
|
||||
void MacroConditionDateEdit::SetupSimpleView()
|
||||
{
|
||||
SetLayoutVisible(_simpleLayout, true);
|
||||
|
||||
@@ -107,6 +107,16 @@ signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
void SetupSimpleView();
|
||||
void SetupAdvancedView();
|
||||
void SetupPatternView();
|
||||
void SetWidgetStatus();
|
||||
void ShowFirstDateSelection(bool visible);
|
||||
void ShowSecondDateSelection(bool visible);
|
||||
|
||||
QComboBox *_weekCondition;
|
||||
DayOfWeekSelector *_days;
|
||||
QCheckBox *_ignoreWeekTime;
|
||||
@@ -134,16 +144,9 @@ protected:
|
||||
QHBoxLayout *_repeatUpdateLayout;
|
||||
QHBoxLayout *_patternLayout;
|
||||
|
||||
std::shared_ptr<MacroConditionDate> _entryData;
|
||||
|
||||
private:
|
||||
void SetupSimpleView();
|
||||
void SetupAdvancedView();
|
||||
void SetupPatternView();
|
||||
void SetWidgetStatus();
|
||||
void ShowFirstDateSelection(bool visible);
|
||||
void ShowSecondDateSelection(bool visible);
|
||||
QTimer _timer;
|
||||
|
||||
std::shared_ptr<MacroConditionDate> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#include "macro-condition-file.hpp"
|
||||
#include "curl-helper.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QTextStream>
|
||||
#include <regex>
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -19,29 +17,6 @@ bool MacroConditionFile::_registered = MacroConditionFactory::Register(
|
||||
|
||||
static std::hash<std::string> strHash;
|
||||
|
||||
static size_t WriteCallback(void *contents, size_t size, size_t nmemb,
|
||||
void *userp)
|
||||
{
|
||||
((std::string *)userp)->append((char *)contents, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
static std::string getRemoteData(std::string &url)
|
||||
{
|
||||
std::string readBuffer;
|
||||
CurlHelper::SetOpt(CURLOPT_URL, url.c_str());
|
||||
CurlHelper::SetOpt(CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
CurlHelper::SetOpt(CURLOPT_WRITEDATA, &readBuffer);
|
||||
// Set timeout to at least one second
|
||||
int timeout = GetIntervalValue() / 1000;
|
||||
if (timeout == 0) {
|
||||
timeout = 1;
|
||||
}
|
||||
CurlHelper::SetOpt(CURLOPT_TIMEOUT, 1);
|
||||
CurlHelper::Perform();
|
||||
return readBuffer;
|
||||
}
|
||||
|
||||
void MacroConditionFile::SetCondition(Condition condition)
|
||||
{
|
||||
_condition = condition;
|
||||
@@ -50,14 +25,6 @@ void MacroConditionFile::SetCondition(Condition condition)
|
||||
|
||||
bool MacroConditionFile::MatchFileContent(QString &filedata)
|
||||
{
|
||||
if (_onlyMatchIfChanged) {
|
||||
size_t newHash = strHash(filedata.toUtf8().constData());
|
||||
if (newHash == _lastHash) {
|
||||
return false;
|
||||
}
|
||||
_lastHash = newHash;
|
||||
}
|
||||
|
||||
if (_regex.Enabled()) {
|
||||
return _regex.Matches(filedata, QString::fromStdString(_text));
|
||||
}
|
||||
@@ -66,31 +33,13 @@ bool MacroConditionFile::MatchFileContent(QString &filedata)
|
||||
return CompareIgnoringLineEnding(text, filedata);
|
||||
}
|
||||
|
||||
bool MacroConditionFile::CheckRemoteFileContent()
|
||||
{
|
||||
std::string path = _file;
|
||||
std::string data = getRemoteData(path);
|
||||
SetVariableValue(data);
|
||||
SetTempVarValue("content", data);
|
||||
QString qdata = QString::fromStdString(data);
|
||||
return MatchFileContent(qdata);
|
||||
}
|
||||
|
||||
bool MacroConditionFile::CheckLocalFileContent()
|
||||
bool MacroConditionFile::CheckFileContent()
|
||||
{
|
||||
QFile file(QString::fromStdString(_file));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_useTime) {
|
||||
QDateTime newLastMod = QFileInfo(file).lastModified();
|
||||
if (_lastMod == newLastMod) {
|
||||
return false;
|
||||
}
|
||||
_lastMod = newLastMod;
|
||||
}
|
||||
|
||||
QString filedata = QTextStream(&file).readAll();
|
||||
SetVariableValue(filedata.toStdString());
|
||||
SetTempVarValue("content", filedata.toStdString());
|
||||
@@ -103,42 +52,30 @@ bool MacroConditionFile::CheckLocalFileContent()
|
||||
bool MacroConditionFile::CheckChangeContent()
|
||||
{
|
||||
QString filedata;
|
||||
switch (_fileType) {
|
||||
case FileType::LOCAL: {
|
||||
std::string path = _file;
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
filedata = QTextStream(&file).readAll();
|
||||
file.close();
|
||||
} break;
|
||||
case FileType::REMOTE: {
|
||||
std::string path = _file;
|
||||
std::string data = getRemoteData(path);
|
||||
QString filedata = QString::fromStdString(data);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
|
||||
std::string path = _file;
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
filedata = QTextStream(&file).readAll();
|
||||
file.close();
|
||||
|
||||
SetTempVarValue("content", filedata.toStdString());
|
||||
size_t newHash = strHash(filedata.toUtf8().constData());
|
||||
const bool contentChanged = newHash != _lastHash;
|
||||
const bool contentChanged = !_firstContentCheck &&
|
||||
(newHash != _lastHash);
|
||||
_lastHash = newHash;
|
||||
_firstContentCheck = false;
|
||||
return contentChanged;
|
||||
}
|
||||
|
||||
bool MacroConditionFile::CheckChangeDate()
|
||||
{
|
||||
if (_fileType == FileType::REMOTE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(QString::fromStdString(_file));
|
||||
QDateTime newLastMod = QFileInfo(file).lastModified();
|
||||
SetVariableValue(newLastMod.toString().toStdString());
|
||||
const bool dateChanged = _lastMod != newLastMod;
|
||||
const bool dateChanged = _lastMod.isValid() && (_lastMod != newLastMod);
|
||||
_lastMod = newLastMod;
|
||||
SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString());
|
||||
return dateChanged;
|
||||
@@ -157,10 +94,6 @@ void MacroConditionFile::SetupTempVars()
|
||||
"AdvSceneSwitcher.tempVar.file.content"));
|
||||
}
|
||||
|
||||
if (_fileType == FileType::REMOTE) {
|
||||
return;
|
||||
}
|
||||
|
||||
AddTempvar(
|
||||
"basename",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.file.basename"),
|
||||
@@ -199,11 +132,7 @@ bool MacroConditionFile::CheckCondition()
|
||||
bool ret = false;
|
||||
switch (_condition) {
|
||||
case Condition::MATCH:
|
||||
if (_fileType == FileType::REMOTE) {
|
||||
ret = CheckRemoteFileContent();
|
||||
break;
|
||||
}
|
||||
ret = CheckLocalFileContent();
|
||||
ret = CheckFileContent();
|
||||
break;
|
||||
case Condition::CONTENT_CHANGE:
|
||||
ret = CheckChangeContent();
|
||||
@@ -264,10 +193,7 @@ bool MacroConditionFile::Save(obs_data_t *obj) const
|
||||
_regex.Save(obj);
|
||||
_file.Save(obj, "file");
|
||||
_text.Save(obj, "text");
|
||||
obs_data_set_int(obj, "fileType", static_cast<int>(_fileType));
|
||||
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
|
||||
obs_data_set_bool(obj, "useTime", _useTime);
|
||||
obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -275,18 +201,10 @@ bool MacroConditionFile::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroCondition::Load(obj);
|
||||
_regex.Load(obj);
|
||||
// TODO: remove in future version
|
||||
if (obs_data_has_user_value(obj, "useRegex")) {
|
||||
_regex.CreateBackwardsCompatibleRegex(
|
||||
obs_data_get_bool(obj, "useRegex"));
|
||||
}
|
||||
_file.Load(obj, "file");
|
||||
_text.Load(obj, "text");
|
||||
_fileType = static_cast<FileType>(obs_data_get_int(obj, "fileType"));
|
||||
SetCondition(
|
||||
static_cast<Condition>(obs_data_get_int(obj, "condition")));
|
||||
_useTime = obs_data_get_bool(obj, "useTime");
|
||||
_onlyMatchIfChanged = obs_data_get_bool(obj, "onlyMatchIfChanged");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -295,13 +213,6 @@ std::string MacroConditionFile::GetShortDesc() const
|
||||
return _file.UnresolvedValue();
|
||||
}
|
||||
|
||||
static void populateFileTypes(QComboBox *list)
|
||||
{
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.condition.file.local"));
|
||||
list->addItem(
|
||||
obs_module_text("AdvSceneSwitcher.condition.file.remote"));
|
||||
}
|
||||
|
||||
static void populateConditions(QComboBox *list)
|
||||
{
|
||||
list->addItem(
|
||||
@@ -321,21 +232,13 @@ static void populateConditions(QComboBox *list)
|
||||
MacroConditionFileEdit::MacroConditionFileEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
|
||||
: QWidget(parent),
|
||||
_fileTypes(new QComboBox()),
|
||||
_conditions(new QComboBox()),
|
||||
_filePath(new FileSelection()),
|
||||
_matchText(new VariableTextEdit(this)),
|
||||
_regex(new RegexConfigWidget(parent)),
|
||||
_checkModificationDate(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.fileTab.checkfileContentTime"))),
|
||||
_checkFileContent(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")))
|
||||
_regex(new RegexConfigWidget(parent))
|
||||
{
|
||||
populateFileTypes(_fileTypes);
|
||||
populateConditions(_conditions);
|
||||
|
||||
QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(FileTypeChanged(int)));
|
||||
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
|
||||
@@ -345,42 +248,20 @@ MacroConditionFileEdit::MacroConditionFileEdit(
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_checkModificationDate, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(CheckModificationDateChanged(int)));
|
||||
QWidget::connect(_checkFileContent, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(OnlyMatchIfChangedChanged(int)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{fileType}}", _fileTypes},
|
||||
{"{{conditions}}", _conditions},
|
||||
{"{{filePath}}", _filePath},
|
||||
{"{{matchText}}", _matchText},
|
||||
{"{{useRegex}}", _regex},
|
||||
{"{{checkModificationDate}}", _checkModificationDate},
|
||||
{"{{checkFileContent}}", _checkFileContent},
|
||||
};
|
||||
auto widgetLayout = new QHBoxLayout;
|
||||
widgetLayout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.file.layout"),
|
||||
widgetLayout,
|
||||
{{"{{conditions}}", _conditions},
|
||||
{"{{filePath}}", _filePath},
|
||||
{"{{regex}}", _regex}});
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout;
|
||||
QHBoxLayout *line1Layout = new QHBoxLayout;
|
||||
QHBoxLayout *line2Layout = new QHBoxLayout;
|
||||
QHBoxLayout *line3Layout = new QHBoxLayout;
|
||||
line1Layout->setContentsMargins(0, 0, 0, 0);
|
||||
line2Layout->setContentsMargins(0, 0, 0, 0);
|
||||
line3Layout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.file.entry.line1"),
|
||||
line1Layout, widgetPlaceholders);
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.file.entry.line2"),
|
||||
line2Layout, widgetPlaceholders, false);
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.file.entry.line3"),
|
||||
line3Layout, widgetPlaceholders);
|
||||
mainLayout->addLayout(line1Layout);
|
||||
mainLayout->addLayout(line2Layout);
|
||||
mainLayout->addLayout(line3Layout);
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addLayout(widgetLayout);
|
||||
layout->addWidget(_matchText);
|
||||
|
||||
setLayout(mainLayout);
|
||||
setLayout(layout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
@@ -393,47 +274,15 @@ void MacroConditionFileEdit::UpdateEntryData()
|
||||
return;
|
||||
}
|
||||
|
||||
_fileTypes->setCurrentIndex(static_cast<int>(_entryData->_fileType));
|
||||
_conditions->setCurrentIndex(
|
||||
static_cast<int>(_entryData->GetCondition()));
|
||||
_filePath->SetPath(_entryData->_file);
|
||||
_matchText->setPlainText(_entryData->_text);
|
||||
_regex->SetRegexConfig(_entryData->_regex);
|
||||
_checkModificationDate->setChecked(_entryData->_useTime);
|
||||
_checkFileContent->setChecked(_entryData->_onlyMatchIfChanged);
|
||||
|
||||
// TODO: Remove in future version
|
||||
if (!_entryData->_useTime) {
|
||||
_checkModificationDate->hide();
|
||||
}
|
||||
if (!_entryData->_onlyMatchIfChanged) {
|
||||
_checkFileContent->hide();
|
||||
}
|
||||
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionFileEdit::FileTypeChanged(int index)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
MacroConditionFile::FileType type =
|
||||
static_cast<MacroConditionFile::FileType>(index);
|
||||
|
||||
if (type == MacroConditionFile::FileType::LOCAL) {
|
||||
_filePath->Button()->setDisabled(false);
|
||||
_checkModificationDate->setDisabled(false);
|
||||
} else {
|
||||
_filePath->Button()->setDisabled(true);
|
||||
_checkModificationDate->setDisabled(true);
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
_entryData->_fileType = type;
|
||||
}
|
||||
|
||||
void MacroConditionFileEdit::ConditionChanged(int index)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
@@ -467,18 +316,6 @@ void MacroConditionFileEdit::RegexChanged(const RegexConfig &conf)
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroConditionFileEdit::CheckModificationDateChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_useTime = state;
|
||||
}
|
||||
|
||||
void MacroConditionFileEdit::OnlyMatchIfChangedChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_onlyMatchIfChanged = state;
|
||||
}
|
||||
|
||||
void MacroConditionFileEdit::SetWidgetVisibility()
|
||||
{
|
||||
if (!_entryData) {
|
||||
@@ -489,20 +326,6 @@ void MacroConditionFileEdit::SetWidgetVisibility()
|
||||
MacroConditionFile::Condition::MATCH);
|
||||
_regex->setVisible(_entryData->GetCondition() ==
|
||||
MacroConditionFile::Condition::MATCH);
|
||||
_checkModificationDate->setVisible(
|
||||
_entryData->_useTime &&
|
||||
_entryData->GetCondition() ==
|
||||
MacroConditionFile::Condition::MATCH);
|
||||
_checkFileContent->setVisible(
|
||||
_entryData->_onlyMatchIfChanged &&
|
||||
_entryData->GetCondition() ==
|
||||
MacroConditionFile::Condition::MATCH);
|
||||
|
||||
// TODO: Remove remote file support in future version in favor of HTTP
|
||||
// action.
|
||||
// Hide the option for now, if it is not used already.
|
||||
_fileTypes->setVisible(_entryData->_fileType ==
|
||||
MacroConditionFile::FileType::REMOTE);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
#include <QDateTime>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -26,11 +23,6 @@ public:
|
||||
return std::make_shared<MacroConditionFile>(m);
|
||||
}
|
||||
|
||||
enum class FileType {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
};
|
||||
|
||||
enum class Condition {
|
||||
MATCH,
|
||||
CONTENT_CHANGE,
|
||||
@@ -46,15 +38,9 @@ public:
|
||||
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
|
||||
RegexConfig _regex;
|
||||
|
||||
// TODO: Remove in future version
|
||||
bool _useTime = false;
|
||||
bool _onlyMatchIfChanged = false;
|
||||
FileType _fileType = FileType::LOCAL;
|
||||
|
||||
private:
|
||||
bool MatchFileContent(QString &filedata);
|
||||
bool CheckRemoteFileContent();
|
||||
bool CheckLocalFileContent();
|
||||
bool CheckFileContent();
|
||||
bool CheckChangeContent();
|
||||
bool CheckChangeDate();
|
||||
void SetupTempVars();
|
||||
@@ -62,6 +48,7 @@ private:
|
||||
Condition _condition = Condition::MATCH;
|
||||
QDateTime _lastMod;
|
||||
size_t _lastHash = 0;
|
||||
bool _firstContentCheck = true;
|
||||
std::string _lastFile;
|
||||
std::string _basename;
|
||||
std::string _basenameComplete;
|
||||
@@ -92,29 +79,22 @@ public:
|
||||
}
|
||||
|
||||
private slots:
|
||||
void FileTypeChanged(int index);
|
||||
void ConditionChanged(int index);
|
||||
void PathChanged(const QString &text);
|
||||
void MatchTextChanged();
|
||||
void RegexChanged(const RegexConfig &);
|
||||
void CheckModificationDateChanged(int state);
|
||||
void OnlyMatchIfChangedChanged(int state);
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
protected:
|
||||
QComboBox *_fileTypes;
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
|
||||
QComboBox *_conditions;
|
||||
FileSelection *_filePath;
|
||||
VariableTextEdit *_matchText;
|
||||
RegexConfigWidget *_regex;
|
||||
QCheckBox *_checkModificationDate;
|
||||
QCheckBox *_checkFileContent;
|
||||
std::shared_ptr<MacroConditionFile> _entryData;
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
|
||||
237
plugins/base/utils/source-interaction-recorder.cpp
Normal file
237
plugins/base/utils/source-interaction-recorder.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include "source-interaction-recorder.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <obs-interaction.h>
|
||||
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace advss {
|
||||
|
||||
SourceInteractionRecorder::SourceInteractionRecorder(QWidget *parent,
|
||||
obs_weak_source_t *source)
|
||||
: QDialog(parent),
|
||||
_source(source),
|
||||
_preview(new SourcePreviewWidget(this, source)),
|
||||
_stepList(new SourceInteractionStepList(this)),
|
||||
_startStopButton(new QPushButton(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.start"),
|
||||
this))
|
||||
{
|
||||
setWindowTitle(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.title"));
|
||||
|
||||
_preview->setMouseTracking(true);
|
||||
_preview->setFocusPolicy(Qt::StrongFocus);
|
||||
_preview->installEventFilter(this);
|
||||
|
||||
_stepList->HideControls();
|
||||
_stepList->SetMinListHeight(50);
|
||||
|
||||
auto buttonRow = new QHBoxLayout;
|
||||
buttonRow->addWidget(_startStopButton);
|
||||
buttonRow->addStretch();
|
||||
|
||||
auto acceptButton = new QPushButton(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.accept"),
|
||||
this);
|
||||
buttonRow->addWidget(acceptButton);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_preview, 1);
|
||||
layout->addWidget(_stepList);
|
||||
layout->addLayout(buttonRow);
|
||||
setLayout(layout);
|
||||
|
||||
connect(_startStopButton, &QPushButton::clicked, this,
|
||||
&SourceInteractionRecorder::StartStop);
|
||||
connect(acceptButton, &QPushButton::clicked, this, [this]() {
|
||||
if (_recording) {
|
||||
StartStop();
|
||||
}
|
||||
emit StepsRecorded(_steps);
|
||||
accept();
|
||||
});
|
||||
}
|
||||
|
||||
SourceInteractionRecorder::~SourceInteractionRecorder() {}
|
||||
|
||||
void SourceInteractionRecorder::StartStop()
|
||||
{
|
||||
_recording = !_recording;
|
||||
if (_recording) {
|
||||
_steps.clear();
|
||||
_stepList->Clear();
|
||||
_firstEvent = true;
|
||||
_startStopButton->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.stop"));
|
||||
} else {
|
||||
_startStopButton->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.start"));
|
||||
}
|
||||
}
|
||||
|
||||
void SourceInteractionRecorder::AppendStep(const SourceInteractionStep &step)
|
||||
{
|
||||
if (!_firstEvent) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
int ms = static_cast<int>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - _lastEventTime)
|
||||
.count());
|
||||
if (ms > 10) {
|
||||
SourceInteractionStep wait;
|
||||
wait.type = SourceInteractionStep::Type::WAIT;
|
||||
wait.waitMs.SetValue(ms);
|
||||
_steps.push_back(wait);
|
||||
_stepList->Insert(
|
||||
QString::fromStdString(wait.ToString()));
|
||||
}
|
||||
}
|
||||
_firstEvent = false;
|
||||
_lastEventTime = std::chrono::steady_clock::now();
|
||||
|
||||
_steps.push_back(step);
|
||||
_stepList->Insert(QString::fromStdString(step.ToString()));
|
||||
_stepList->ScrollToBottom();
|
||||
}
|
||||
|
||||
bool SourceInteractionRecorder::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj != _preview || !_recording) {
|
||||
return QDialog::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::MouseButtonPress:
|
||||
case QEvent::MouseButtonRelease:
|
||||
case QEvent::MouseButtonDblClick:
|
||||
return HandleMouseClick(static_cast<QMouseEvent *>(event));
|
||||
case QEvent::MouseMove:
|
||||
return HandleMouseMove(static_cast<QMouseEvent *>(event));
|
||||
case QEvent::Wheel:
|
||||
return HandleMouseWheel(static_cast<QWheelEvent *>(event));
|
||||
case QEvent::KeyPress:
|
||||
case QEvent::KeyRelease:
|
||||
return HandleKeyEvent(static_cast<QKeyEvent *>(event));
|
||||
case QEvent::FocusIn:
|
||||
case QEvent::FocusOut: {
|
||||
OBSSourceAutoRelease source =
|
||||
obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
obs_source_send_focus(source,
|
||||
event->type() == QEvent::FocusIn);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QDialog::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
bool SourceInteractionRecorder::HandleMouseClick(QMouseEvent *event)
|
||||
{
|
||||
SourceInteractionStep step;
|
||||
step.type = SourceInteractionStep::Type::MOUSE_CLICK;
|
||||
|
||||
int srcX = 0, srcY = 0;
|
||||
QPoint pos = event->pos();
|
||||
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
|
||||
step.x.SetValue(srcX);
|
||||
step.y.SetValue(srcY);
|
||||
|
||||
step.mouseUp = (event->type() == QEvent::MouseButtonRelease);
|
||||
step.clickCount.SetValue(
|
||||
(event->type() == QEvent::MouseButtonDblClick) ? 2 : 1);
|
||||
|
||||
switch (event->button()) {
|
||||
case Qt::LeftButton:
|
||||
step.button = MOUSE_LEFT;
|
||||
break;
|
||||
case Qt::MiddleButton:
|
||||
step.button = MOUSE_MIDDLE;
|
||||
break;
|
||||
case Qt::RightButton:
|
||||
step.button = MOUSE_RIGHT;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
AppendStep(step);
|
||||
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
PerformInteractionStep(source, step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SourceInteractionRecorder::HandleMouseMove(QMouseEvent *event)
|
||||
{
|
||||
SourceInteractionStep step;
|
||||
step.type = SourceInteractionStep::Type::MOUSE_MOVE;
|
||||
int srcX = 0, srcY = 0;
|
||||
QPoint pos = event->pos();
|
||||
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
|
||||
step.x.SetValue(srcX);
|
||||
step.y.SetValue(srcY);
|
||||
|
||||
// Always forward moves to the source so hover/cursor effects work,
|
||||
// but only record them when a button is held (drag), to reduce noise.
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
PerformInteractionStep(source, step);
|
||||
}
|
||||
|
||||
if (event->buttons() != Qt::NoButton) {
|
||||
AppendStep(step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SourceInteractionRecorder::HandleMouseWheel(QWheelEvent *event)
|
||||
{
|
||||
SourceInteractionStep step;
|
||||
step.type = SourceInteractionStep::Type::MOUSE_WHEEL;
|
||||
int srcX = 0, srcY = 0;
|
||||
const QPointF pos = event->position();
|
||||
_preview->GetSourceRelativeXY((int)pos.x(), (int)pos.y(), srcX, srcY);
|
||||
step.x.SetValue(srcX);
|
||||
step.y.SetValue(srcY);
|
||||
const QPoint angle = event->angleDelta();
|
||||
step.wheelDeltaX.SetValue(angle.x());
|
||||
step.wheelDeltaY.SetValue(angle.y());
|
||||
AppendStep(step);
|
||||
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
PerformInteractionStep(source, step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SourceInteractionRecorder::HandleKeyEvent(QKeyEvent *event)
|
||||
{
|
||||
SourceInteractionStep step;
|
||||
step.type = SourceInteractionStep::Type::KEY_PRESS;
|
||||
step.keyUp = (event->type() == QEvent::KeyRelease);
|
||||
step.nativeVkey.SetValue(static_cast<int>(event->nativeVirtualKey()));
|
||||
step.text = event->text().toStdString();
|
||||
AppendStep(step);
|
||||
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
PerformInteractionStep(source, step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
54
plugins/base/utils/source-interaction-recorder.hpp
Normal file
54
plugins/base/utils/source-interaction-recorder.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include "source-interaction-step.hpp"
|
||||
#include "source-interaction-step-list.hpp"
|
||||
#include "source-preview-widget.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPushButton>
|
||||
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
namespace advss {
|
||||
|
||||
class SourceInteractionRecorder : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SourceInteractionRecorder(QWidget *parent, obs_weak_source_t *source);
|
||||
~SourceInteractionRecorder();
|
||||
|
||||
const std::vector<SourceInteractionStep> &GetSteps() const
|
||||
{
|
||||
return _steps;
|
||||
}
|
||||
|
||||
signals:
|
||||
void StepsRecorded(const std::vector<SourceInteractionStep> &);
|
||||
|
||||
private slots:
|
||||
void StartStop();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
bool HandleMouseClick(QMouseEvent *);
|
||||
bool HandleMouseMove(QMouseEvent *);
|
||||
bool HandleMouseWheel(QWheelEvent *);
|
||||
bool HandleKeyEvent(QKeyEvent *);
|
||||
void AppendStep(const SourceInteractionStep &);
|
||||
|
||||
obs_weak_source_t *_source;
|
||||
SourcePreviewWidget *_preview;
|
||||
SourceInteractionStepList *_stepList;
|
||||
QPushButton *_startStopButton;
|
||||
|
||||
bool _recording = false;
|
||||
std::vector<SourceInteractionStep> _steps;
|
||||
std::chrono::steady_clock::time_point _lastEventTime;
|
||||
bool _firstEvent = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
315
plugins/base/utils/source-interaction-step-edit.cpp
Normal file
315
plugins/base/utils/source-interaction-step-edit.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
#include "source-interaction-step-edit.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static const std::map<SourceInteractionStep::Type, std::string> stepTypeNames = {
|
||||
{SourceInteractionStep::Type::MOUSE_MOVE,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove"},
|
||||
{SourceInteractionStep::Type::MOUSE_CLICK,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick"},
|
||||
{SourceInteractionStep::Type::MOUSE_WHEEL,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel"},
|
||||
{SourceInteractionStep::Type::KEY_PRESS,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress"},
|
||||
{SourceInteractionStep::Type::TYPE_TEXT,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText"},
|
||||
{SourceInteractionStep::Type::WAIT,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wait"},
|
||||
};
|
||||
|
||||
static const std::map<obs_mouse_button_type, std::string> mouseButtonNames = {
|
||||
{MOUSE_LEFT, "AdvSceneSwitcher.action.sourceInteraction.button.left"},
|
||||
{MOUSE_MIDDLE,
|
||||
"AdvSceneSwitcher.action.sourceInteraction.button.middle"},
|
||||
{MOUSE_RIGHT, "AdvSceneSwitcher.action.sourceInteraction.button.right"},
|
||||
};
|
||||
|
||||
static void populateTypeCombo(QComboBox *combo)
|
||||
{
|
||||
for (const auto &[type, name] : stepTypeNames) {
|
||||
combo->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(type));
|
||||
}
|
||||
}
|
||||
|
||||
static void populateButtonCombo(QComboBox *combo)
|
||||
{
|
||||
for (const auto &[btn, name] : mouseButtonNames) {
|
||||
combo->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(btn));
|
||||
}
|
||||
}
|
||||
|
||||
static QHBoxLayout *labelledRow(const char *labelKey, QWidget *w)
|
||||
{
|
||||
auto row = new QHBoxLayout;
|
||||
row->addWidget(new QLabel(obs_module_text(labelKey)));
|
||||
row->addWidget(w);
|
||||
row->addStretch();
|
||||
return row;
|
||||
}
|
||||
|
||||
SourceInteractionStepEdit::SourceInteractionStepEdit(
|
||||
QWidget *parent, const SourceInteractionStep &step)
|
||||
: QWidget(parent),
|
||||
_typeCombo(new QComboBox(this)),
|
||||
_fields(new QStackedWidget(this)),
|
||||
_step(step)
|
||||
{
|
||||
populateTypeCombo(_typeCombo);
|
||||
|
||||
auto mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto typeRow = labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.type",
|
||||
_typeCombo);
|
||||
mainLayout->addLayout(typeRow);
|
||||
mainLayout->addWidget(_fields);
|
||||
setLayout(mainLayout);
|
||||
|
||||
connect(_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &SourceInteractionStepEdit::TypeChanged);
|
||||
|
||||
RebuildFields();
|
||||
|
||||
int idx = _typeCombo->findData(static_cast<int>(_step.type));
|
||||
if (idx >= 0) {
|
||||
_typeCombo->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceInteractionStepEdit::TypeChanged(int)
|
||||
{
|
||||
_step.type = static_cast<SourceInteractionStep::Type>(
|
||||
_typeCombo->currentData().toInt());
|
||||
RebuildFields();
|
||||
emit StepChanged(_step);
|
||||
}
|
||||
|
||||
void SourceInteractionStepEdit::UpdateStep()
|
||||
{
|
||||
emit StepChanged(_step);
|
||||
}
|
||||
|
||||
void SourceInteractionStepEdit::RebuildFields()
|
||||
{
|
||||
while (_fields->count() > 0) {
|
||||
auto w = _fields->widget(0);
|
||||
_fields->removeWidget(w);
|
||||
delete w;
|
||||
}
|
||||
|
||||
auto makeVarSpin = [](int min, int max,
|
||||
const NumberVariable<int> &val) {
|
||||
auto sb = new VariableSpinBox;
|
||||
sb->setMinimum(min);
|
||||
sb->setMaximum(max);
|
||||
sb->SetValue(val);
|
||||
return sb;
|
||||
};
|
||||
|
||||
switch (_step.type) {
|
||||
case SourceInteractionStep::Type::MOUSE_MOVE:
|
||||
case SourceInteractionStep::Type::MOUSE_CLICK:
|
||||
case SourceInteractionStep::Type::MOUSE_WHEEL: {
|
||||
auto page = new QWidget;
|
||||
auto layout = new QVBoxLayout(page);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto xSpin = makeVarSpin(-32768, 32767, _step.x);
|
||||
auto ySpin = makeVarSpin(-32768, 32767, _step.y);
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.x",
|
||||
xSpin));
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.y",
|
||||
ySpin));
|
||||
|
||||
connect(xSpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::NumberVariableChanged),
|
||||
this, [this, xSpin](const NumberVariable<int> &) {
|
||||
_step.x = xSpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
connect(ySpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::NumberVariableChanged),
|
||||
this, [this, ySpin](const NumberVariable<int> &) {
|
||||
_step.y = ySpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
|
||||
if (_step.type == SourceInteractionStep::Type::MOUSE_CLICK) {
|
||||
auto btnCombo = new QComboBox;
|
||||
populateButtonCombo(btnCombo);
|
||||
btnCombo->setCurrentIndex(btnCombo->findData(
|
||||
static_cast<int>(_step.button)));
|
||||
|
||||
auto upCheck = new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp"));
|
||||
upCheck->setChecked(_step.mouseUp);
|
||||
|
||||
auto countSpin = makeVarSpin(1, 3, _step.clickCount);
|
||||
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.button",
|
||||
btnCombo));
|
||||
layout->addWidget(upCheck);
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount",
|
||||
countSpin));
|
||||
|
||||
connect(btnCombo,
|
||||
QOverload<int>::of(
|
||||
&QComboBox::currentIndexChanged),
|
||||
this, [this, btnCombo]() {
|
||||
_step.button = static_cast<
|
||||
obs_mouse_button_type>(
|
||||
btnCombo->currentData().toInt());
|
||||
UpdateStep();
|
||||
});
|
||||
connect(upCheck, &QCheckBox::stateChanged, this,
|
||||
[this, upCheck]() {
|
||||
_step.mouseUp = upCheck->isChecked();
|
||||
UpdateStep();
|
||||
});
|
||||
connect(countSpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::
|
||||
NumberVariableChanged),
|
||||
this,
|
||||
[this, countSpin](const NumberVariable<int> &) {
|
||||
_step.clickCount = countSpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
} else if (_step.type ==
|
||||
SourceInteractionStep::Type::MOUSE_WHEEL) {
|
||||
auto dxSpin =
|
||||
makeVarSpin(-1200, 1200, _step.wheelDeltaX);
|
||||
auto dySpin =
|
||||
makeVarSpin(-1200, 1200, _step.wheelDeltaY);
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx",
|
||||
dxSpin));
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy",
|
||||
dySpin));
|
||||
connect(dxSpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::
|
||||
NumberVariableChanged),
|
||||
this,
|
||||
[this, dxSpin](const NumberVariable<int> &) {
|
||||
_step.wheelDeltaX = dxSpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
connect(dySpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::
|
||||
NumberVariableChanged),
|
||||
this,
|
||||
[this, dySpin](const NumberVariable<int> &) {
|
||||
_step.wheelDeltaY = dySpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
}
|
||||
|
||||
_fields->addWidget(page);
|
||||
_fields->setCurrentWidget(page);
|
||||
break;
|
||||
}
|
||||
|
||||
case SourceInteractionStep::Type::KEY_PRESS: {
|
||||
auto page = new QWidget;
|
||||
auto layout = new QVBoxLayout(page);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto vkeySpin = makeVarSpin(0, 0xFFFF, _step.nativeVkey);
|
||||
auto upCheck = new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp"));
|
||||
upCheck->setChecked(_step.keyUp);
|
||||
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey",
|
||||
vkeySpin));
|
||||
layout->addWidget(upCheck);
|
||||
|
||||
connect(vkeySpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::NumberVariableChanged),
|
||||
this, [this, vkeySpin](const NumberVariable<int> &) {
|
||||
_step.nativeVkey = vkeySpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
connect(upCheck, &QCheckBox::stateChanged, this,
|
||||
[this, upCheck]() {
|
||||
_step.keyUp = upCheck->isChecked();
|
||||
UpdateStep();
|
||||
});
|
||||
|
||||
_fields->addWidget(page);
|
||||
_fields->setCurrentWidget(page);
|
||||
break;
|
||||
}
|
||||
|
||||
case SourceInteractionStep::Type::TYPE_TEXT: {
|
||||
auto page = new QWidget;
|
||||
auto layout = new QVBoxLayout(page);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto textEdit = new VariableLineEdit(page);
|
||||
textEdit->setText(_step.text);
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.text",
|
||||
textEdit));
|
||||
|
||||
connect(textEdit, &QLineEdit::editingFinished, this,
|
||||
[this, textEdit]() {
|
||||
_step.text = textEdit->text().toStdString();
|
||||
UpdateStep();
|
||||
});
|
||||
|
||||
_fields->addWidget(page);
|
||||
_fields->setCurrentWidget(page);
|
||||
break;
|
||||
}
|
||||
|
||||
case SourceInteractionStep::Type::WAIT: {
|
||||
auto page = new QWidget;
|
||||
auto layout = new QVBoxLayout(page);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto msSpin = makeVarSpin(0, 60000, _step.waitMs);
|
||||
layout->addLayout(labelledRow(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs",
|
||||
msSpin));
|
||||
|
||||
connect(msSpin,
|
||||
QOverload<const NumberVariable<int> &>::of(
|
||||
&GenericVariableSpinbox::NumberVariableChanged),
|
||||
this, [this, msSpin](const NumberVariable<int> &) {
|
||||
_step.waitMs = msSpin->Value();
|
||||
UpdateStep();
|
||||
});
|
||||
|
||||
_fields->addWidget(page);
|
||||
_fields->setCurrentWidget(page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
32
plugins/base/utils/source-interaction-step-edit.hpp
Normal file
32
plugins/base/utils/source-interaction-step-edit.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "source-interaction-step.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class SourceInteractionStepEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SourceInteractionStepEdit(QWidget *parent,
|
||||
const SourceInteractionStep &step);
|
||||
SourceInteractionStep GetStep() const { return _step; }
|
||||
|
||||
signals:
|
||||
void StepChanged(const SourceInteractionStep &);
|
||||
|
||||
private slots:
|
||||
void TypeChanged(int);
|
||||
void UpdateStep();
|
||||
|
||||
private:
|
||||
void RebuildFields();
|
||||
|
||||
QComboBox *_typeCombo;
|
||||
QStackedWidget *_fields;
|
||||
SourceInteractionStep _step;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
99
plugins/base/utils/source-interaction-step-list.cpp
Normal file
99
plugins/base/utils/source-interaction-step-list.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "source-interaction-step-list.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace advss {
|
||||
|
||||
SourceInteractionStepList::SourceInteractionStepList(QWidget *parent)
|
||||
: ListEditor(parent)
|
||||
{
|
||||
_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
connect(_list, &QListWidget::currentRowChanged, this,
|
||||
&SourceInteractionStepList::RowSelected);
|
||||
SetPlaceholderText(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.record.placeholder"));
|
||||
SetMaxListHeight(350);
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::SetSteps(
|
||||
const std::vector<SourceInteractionStep> &steps)
|
||||
{
|
||||
_steps = steps;
|
||||
|
||||
_list->clear();
|
||||
for (const auto &step : _steps) {
|
||||
_list->addItem(QString::fromStdString(step.ToString()));
|
||||
}
|
||||
|
||||
UpdateListSize();
|
||||
|
||||
_list->setCurrentRow(-1);
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::UpdateStep(int row,
|
||||
const SourceInteractionStep &step)
|
||||
{
|
||||
if (row < 0 || row >= (int)_steps.size()) {
|
||||
return;
|
||||
}
|
||||
_steps[row] = step;
|
||||
_list->item(row)->setText(QString::fromStdString(step.ToString()));
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::Add()
|
||||
{
|
||||
_steps.emplace_back();
|
||||
_list->addItem(QString::fromStdString(_steps.back().ToString()));
|
||||
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
|
||||
emit StepsChanged(_steps);
|
||||
_list->setCurrentRow((int)_steps.size() - 1);
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::Remove()
|
||||
{
|
||||
const QList<QListWidgetItem *> selected = _list->selectedItems();
|
||||
if (selected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
std::vector<int> rows;
|
||||
rows.reserve(selected.size());
|
||||
for (const auto *item : selected) {
|
||||
rows.push_back(_list->row(item));
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(), std::greater<int>());
|
||||
for (int row : rows) {
|
||||
if (row >= 0 && row < (int)_steps.size()) {
|
||||
_steps.erase(_steps.begin() + row);
|
||||
delete _list->takeItem(row);
|
||||
}
|
||||
}
|
||||
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
|
||||
emit StepsChanged(_steps);
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::Up()
|
||||
{
|
||||
int row = _list->currentRow();
|
||||
if (row <= 0 || row >= (int)_steps.size()) {
|
||||
return;
|
||||
}
|
||||
std::swap(_steps[row], _steps[row - 1]);
|
||||
_list->insertItem(row - 1, _list->takeItem(row));
|
||||
_list->setCurrentRow(row - 1);
|
||||
emit StepsChanged(_steps);
|
||||
}
|
||||
|
||||
void SourceInteractionStepList::Down()
|
||||
{
|
||||
int row = _list->currentRow();
|
||||
if (row < 0 || row >= (int)_steps.size() - 1) {
|
||||
return;
|
||||
}
|
||||
std::swap(_steps[row], _steps[row + 1]);
|
||||
_list->insertItem(row + 1, _list->takeItem(row));
|
||||
_list->setCurrentRow(row + 1);
|
||||
emit StepsChanged(_steps);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
38
plugins/base/utils/source-interaction-step-list.hpp
Normal file
38
plugins/base/utils/source-interaction-step-list.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "list-editor.hpp"
|
||||
#include "source-interaction-step.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class SourceInteractionStepList : public ListEditor {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SourceInteractionStepList(QWidget *parent = nullptr);
|
||||
|
||||
void SetSteps(const std::vector<SourceInteractionStep> &steps);
|
||||
void UpdateStep(int row, const SourceInteractionStep &step);
|
||||
int CurrentRow() const { return _list->currentRow(); }
|
||||
void SetCurrentRow(int row) { _list->setCurrentRow(row); }
|
||||
void Clear() const { _list->clear(); }
|
||||
void HideControls() const { _controls->hide(); }
|
||||
void Insert(const QString &value) const { _list->addItem(value); }
|
||||
void ScrollToBottom() const { _list->scrollToBottom(); }
|
||||
void AddControlWidget(QWidget *widget) { _controls->AddWidget(widget); }
|
||||
|
||||
signals:
|
||||
void StepsChanged(const std::vector<SourceInteractionStep> &);
|
||||
void RowSelected(int row);
|
||||
|
||||
private slots:
|
||||
void Add() override;
|
||||
void Remove() override;
|
||||
void Up() override;
|
||||
void Down() override;
|
||||
|
||||
private:
|
||||
std::vector<SourceInteractionStep> _steps;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
190
plugins/base/utils/source-interaction-step.cpp
Normal file
190
plugins/base/utils/source-interaction-step.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "source-interaction-step.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs-interaction.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static QString varOrNum(const NumberVariable<int> &v)
|
||||
{
|
||||
if (!v.IsFixedType()) {
|
||||
return QString::fromStdString(
|
||||
GetWeakVariableName(v.GetVariable()));
|
||||
}
|
||||
return QString::number(v.GetFixedValue());
|
||||
}
|
||||
|
||||
bool SourceInteractionStep::Save(obs_data_t *obj) const
|
||||
{
|
||||
obs_data_set_int(obj, "type", static_cast<int>(type));
|
||||
x.Save(obj, "x");
|
||||
y.Save(obj, "y");
|
||||
obs_data_set_int(obj, "button", static_cast<int>(button));
|
||||
obs_data_set_bool(obj, "mouseUp", mouseUp);
|
||||
clickCount.Save(obj, "clickCount");
|
||||
wheelDeltaX.Save(obj, "wheelDeltaX");
|
||||
wheelDeltaY.Save(obj, "wheelDeltaY");
|
||||
nativeVkey.Save(obj, "nativeVkey");
|
||||
obs_data_set_int(obj, "modifiers", modifiers);
|
||||
obs_data_set_bool(obj, "keyUp", keyUp);
|
||||
text.Save(obj, "text");
|
||||
waitMs.Save(obj, "waitMs");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SourceInteractionStep::Load(obs_data_t *obj)
|
||||
{
|
||||
type = static_cast<Type>(obs_data_get_int(obj, "type"));
|
||||
x.Load(obj, "x");
|
||||
y.Load(obj, "y");
|
||||
button = static_cast<obs_mouse_button_type>(
|
||||
obs_data_get_int(obj, "button"));
|
||||
mouseUp = obs_data_get_bool(obj, "mouseUp");
|
||||
clickCount.Load(obj, "clickCount");
|
||||
wheelDeltaX.Load(obj, "wheelDeltaX");
|
||||
wheelDeltaY.Load(obj, "wheelDeltaY");
|
||||
nativeVkey.Load(obj, "nativeVkey");
|
||||
modifiers = static_cast<uint32_t>(obs_data_get_int(obj, "modifiers"));
|
||||
keyUp = obs_data_get_bool(obj, "keyUp");
|
||||
text.Load(obj, "text");
|
||||
waitMs.Load(obj, "waitMs");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SourceInteractionStep::ToString() const
|
||||
{
|
||||
switch (type) {
|
||||
case Type::MOUSE_MOVE:
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove"))
|
||||
.arg(varOrNum(x))
|
||||
.arg(varOrNum(y))
|
||||
.toStdString();
|
||||
case Type::MOUSE_CLICK: {
|
||||
const char *btnKey =
|
||||
(button == MOUSE_LEFT)
|
||||
? "AdvSceneSwitcher.action.sourceInteraction.button.left"
|
||||
: (button == MOUSE_MIDDLE)
|
||||
? "AdvSceneSwitcher.action.sourceInteraction.button.middle"
|
||||
: "AdvSceneSwitcher.action.sourceInteraction.button.right";
|
||||
const char *dirKey =
|
||||
mouseUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp"
|
||||
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown";
|
||||
QString result =
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick"))
|
||||
.arg(obs_module_text(dirKey))
|
||||
.arg(obs_module_text(btnKey))
|
||||
.arg(varOrNum(x))
|
||||
.arg(varOrNum(y));
|
||||
if (!clickCount.IsFixedType() ||
|
||||
clickCount.GetFixedValue() > 1) {
|
||||
result +=
|
||||
QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount"))
|
||||
.arg(varOrNum(clickCount));
|
||||
}
|
||||
return result.toStdString();
|
||||
}
|
||||
case Type::MOUSE_WHEEL:
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel"))
|
||||
.arg(varOrNum(x))
|
||||
.arg(varOrNum(y))
|
||||
.arg(varOrNum(wheelDeltaX))
|
||||
.arg(varOrNum(wheelDeltaY))
|
||||
.toStdString();
|
||||
case Type::KEY_PRESS: {
|
||||
const char *dirKey =
|
||||
keyUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp"
|
||||
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown";
|
||||
if (text.empty()) {
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress"))
|
||||
.arg(obs_module_text(dirKey))
|
||||
.toStdString();
|
||||
}
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText"))
|
||||
.arg(obs_module_text(dirKey))
|
||||
.arg(QString::fromStdString(text))
|
||||
.toStdString();
|
||||
}
|
||||
case Type::TYPE_TEXT:
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText"))
|
||||
.arg(QString::fromStdString(text))
|
||||
.toStdString();
|
||||
case Type::WAIT:
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait"))
|
||||
.arg(varOrNum(waitMs))
|
||||
.toStdString();
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void PerformInteractionStep(obs_source_t *source,
|
||||
const SourceInteractionStep &step)
|
||||
{
|
||||
switch (step.type) {
|
||||
case SourceInteractionStep::Type::MOUSE_MOVE: {
|
||||
obs_mouse_event e{};
|
||||
e.x = step.x;
|
||||
e.y = step.y;
|
||||
obs_source_send_mouse_move(source, &e, false);
|
||||
break;
|
||||
}
|
||||
case SourceInteractionStep::Type::MOUSE_CLICK: {
|
||||
obs_mouse_event e{};
|
||||
e.modifiers = step.modifiers;
|
||||
e.x = step.x;
|
||||
e.y = step.y;
|
||||
obs_source_send_mouse_click(source, &e, step.button,
|
||||
step.mouseUp, step.clickCount);
|
||||
break;
|
||||
}
|
||||
case SourceInteractionStep::Type::MOUSE_WHEEL: {
|
||||
obs_mouse_event e{};
|
||||
e.modifiers = step.modifiers;
|
||||
e.x = step.x;
|
||||
e.y = step.y;
|
||||
obs_source_send_mouse_wheel(source, &e, step.wheelDeltaX,
|
||||
step.wheelDeltaY);
|
||||
break;
|
||||
}
|
||||
case SourceInteractionStep::Type::KEY_PRESS: {
|
||||
std::string textCopy = step.text;
|
||||
obs_key_event e{};
|
||||
e.modifiers = step.modifiers;
|
||||
e.text = textCopy.data();
|
||||
e.native_vkey =
|
||||
static_cast<uint32_t>(step.nativeVkey.GetValue());
|
||||
obs_source_send_key_click(source, &e, step.keyUp);
|
||||
break;
|
||||
}
|
||||
case SourceInteractionStep::Type::TYPE_TEXT: {
|
||||
const std::string resolvedText = step.text;
|
||||
for (const char c : resolvedText) {
|
||||
std::string ch(1, c);
|
||||
obs_key_event e{};
|
||||
e.text = ch.data();
|
||||
obs_source_send_key_click(source, &e, false);
|
||||
obs_source_send_key_click(source, &e, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SourceInteractionStep::Type::WAIT:
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(step.waitMs));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
48
plugins/base/utils/source-interaction-step.hpp
Normal file
48
plugins/base/utils/source-interaction-step.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "variable-number.hpp"
|
||||
#include "variable-string.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <obs-interaction.h>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct SourceInteractionStep {
|
||||
enum class Type {
|
||||
MOUSE_MOVE,
|
||||
MOUSE_CLICK,
|
||||
MOUSE_WHEEL,
|
||||
KEY_PRESS,
|
||||
TYPE_TEXT,
|
||||
WAIT,
|
||||
};
|
||||
|
||||
Type type = Type::MOUSE_MOVE;
|
||||
|
||||
// Mouse fields
|
||||
NumberVariable<int> x = 0;
|
||||
NumberVariable<int> y = 0;
|
||||
obs_mouse_button_type button = MOUSE_LEFT;
|
||||
bool mouseUp = false;
|
||||
NumberVariable<int> clickCount = 1;
|
||||
NumberVariable<int> wheelDeltaX = 0;
|
||||
NumberVariable<int> wheelDeltaY = 120;
|
||||
|
||||
// Key / text fields
|
||||
NumberVariable<int> nativeVkey = 0;
|
||||
uint32_t modifiers = 0; // INTERACT_*
|
||||
bool keyUp = false;
|
||||
StringVariable text; // for TYPE_TEXT or key text
|
||||
|
||||
// Wait field (ms)
|
||||
NumberVariable<int> waitMs = 100;
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
void PerformInteractionStep(obs_source_t *source,
|
||||
const SourceInteractionStep &step);
|
||||
|
||||
} // namespace advss
|
||||
149
plugins/base/utils/source-preview-widget.cpp
Normal file
149
plugins/base/utils/source-preview-widget.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "source-preview-widget.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <graphics/graphics.h>
|
||||
|
||||
#include <QResizeEvent>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static int32_t clampToSource(int32_t v, uint32_t max)
|
||||
{
|
||||
if (v < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (v > (int32_t)max) {
|
||||
return (int32_t)max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
SourcePreviewWidget::SourcePreviewWidget(QWidget *parent,
|
||||
obs_weak_source_t *source)
|
||||
: QWidget(parent),
|
||||
_source(source)
|
||||
{
|
||||
setAttribute(Qt::WA_PaintOnScreen);
|
||||
setAttribute(Qt::WA_NoSystemBackground);
|
||||
setMinimumSize(320, 240);
|
||||
}
|
||||
|
||||
SourcePreviewWidget::~SourcePreviewWidget()
|
||||
{
|
||||
if (_display) {
|
||||
obs_display_remove_draw_callback(
|
||||
_display, SourcePreviewWidget::DrawCallback, this);
|
||||
obs_display_destroy(_display);
|
||||
_display = nullptr;
|
||||
}
|
||||
|
||||
if (_showing) {
|
||||
OBSSourceAutoRelease source =
|
||||
obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
obs_source_dec_showing(source);
|
||||
}
|
||||
_showing = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SourcePreviewWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
|
||||
if (!_display) {
|
||||
WId wid = winId();
|
||||
if (!wid) {
|
||||
return;
|
||||
}
|
||||
|
||||
gs_init_data info{};
|
||||
#if defined(_WIN32)
|
||||
info.window.hwnd = reinterpret_cast<void *>(wid);
|
||||
#elif defined(__APPLE__)
|
||||
info.window.view = reinterpret_cast<id>(wid);
|
||||
#else
|
||||
info.window.id = static_cast<uint32_t>(wid);
|
||||
info.window.display = nullptr;
|
||||
#endif
|
||||
info.format = GS_BGRA;
|
||||
info.zsformat = GS_ZS_NONE;
|
||||
info.cx = static_cast<uint32_t>(width());
|
||||
info.cy = static_cast<uint32_t>(height());
|
||||
|
||||
_display = obs_display_create(&info, 0x000000);
|
||||
if (_display) {
|
||||
obs_display_add_draw_callback(
|
||||
_display, SourcePreviewWidget::DrawCallback,
|
||||
this);
|
||||
OBSSourceAutoRelease source =
|
||||
obs_weak_source_get_source(_source);
|
||||
if (source) {
|
||||
obs_source_inc_showing(source);
|
||||
_showing = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obs_display_resize(_display, static_cast<uint32_t>(width()),
|
||||
static_cast<uint32_t>(height()));
|
||||
}
|
||||
}
|
||||
|
||||
void SourcePreviewWidget::DrawCallback(void *param, uint32_t cx, uint32_t cy)
|
||||
{
|
||||
auto self = static_cast<SourcePreviewWidget *>(param);
|
||||
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(self->_source);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
|
||||
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
|
||||
|
||||
float scaleX = static_cast<float>(cx) / srcW;
|
||||
float scaleY = static_cast<float>(cy) / srcH;
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
|
||||
int newW = static_cast<int>(scale * srcW);
|
||||
int newH = static_cast<int>(scale * srcH);
|
||||
int offX = (static_cast<int>(cx) - newW) / 2;
|
||||
int offY = (static_cast<int>(cy) - newH) / 2;
|
||||
|
||||
self->_offsetX = offX;
|
||||
self->_offsetY = offY;
|
||||
self->_scale = scale;
|
||||
|
||||
gs_viewport_push();
|
||||
gs_projection_push();
|
||||
gs_ortho(0.0f, static_cast<float>(srcW), 0.0f, static_cast<float>(srcH),
|
||||
-100.0f, 100.0f);
|
||||
gs_set_viewport(offX, offY, newW, newH);
|
||||
|
||||
obs_source_video_render(source);
|
||||
|
||||
gs_projection_pop();
|
||||
gs_viewport_pop();
|
||||
}
|
||||
|
||||
bool SourcePreviewWidget::GetSourceRelativeXY(int widgetX, int widgetY,
|
||||
int &srcX, int &srcY) const
|
||||
{
|
||||
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
|
||||
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
|
||||
|
||||
srcX = static_cast<int>((widgetX - _offsetX) / _scale);
|
||||
srcY = static_cast<int>((widgetY - _offsetY) / _scale);
|
||||
srcX = static_cast<int>(
|
||||
clampToSource(static_cast<int32_t>(srcX), srcW));
|
||||
srcY = static_cast<int>(
|
||||
clampToSource(static_cast<int32_t>(srcY), srcH));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
40
plugins/base/utils/source-preview-widget.hpp
Normal file
40
plugins/base/utils/source-preview-widget.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.h>
|
||||
#include <graphics/graphics.h>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QResizeEvent;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// A native window widget that renders an OBS source via obs_display.
|
||||
// Stores letterbox geometry for coordinate mapping.
|
||||
class SourcePreviewWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SourcePreviewWidget(QWidget *parent, obs_weak_source_t *source);
|
||||
~SourcePreviewWidget();
|
||||
|
||||
// Maps a widget-local point to source-space coordinates.
|
||||
bool GetSourceRelativeXY(int widgetX, int widgetY, int &srcX,
|
||||
int &srcY) const;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *) override;
|
||||
QPaintEngine *paintEngine() const override { return nullptr; }
|
||||
|
||||
private:
|
||||
static void DrawCallback(void *param, uint32_t cx, uint32_t cy);
|
||||
|
||||
obs_weak_source_t *_source;
|
||||
obs_display_t *_display = nullptr;
|
||||
bool _showing = false;
|
||||
|
||||
int _offsetX = 0;
|
||||
int _offsetY = 0;
|
||||
float _scale = 1.0f;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -48,8 +48,17 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp
|
||||
key-value-list.cpp key-value-list.hpp)
|
||||
${PROJECT_NAME}
|
||||
PRIVATE macro-action-http.cpp
|
||||
macro-action-http.hpp
|
||||
macro-condition-http.cpp
|
||||
macro-condition-http.hpp
|
||||
http-server.cpp
|
||||
http-server.hpp
|
||||
http-server-tab.cpp
|
||||
http-server-tab.hpp
|
||||
key-value-list.cpp
|
||||
key-value-list.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
223
plugins/http/http-server-tab.cpp
Normal file
223
plugins/http/http-server-tab.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
#include "http-server-tab.hpp"
|
||||
#include "http-server.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "tab-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QTabWidget>
|
||||
#include <QTimer>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool registerTab();
|
||||
static void setupTab(QTabWidget *);
|
||||
static bool registerTabDone = registerTab();
|
||||
|
||||
static HttpServersTable *tabWidget = nullptr;
|
||||
|
||||
static bool registerTab()
|
||||
{
|
||||
AddSetupTabCallback("httpServerTab", HttpServersTable::Create,
|
||||
setupTab);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void setTabVisible(QTabWidget *tab, bool visible)
|
||||
{
|
||||
SetTabVisibleByName(
|
||||
tab, visible,
|
||||
obs_module_text("AdvSceneSwitcher.httpServerTab.title"));
|
||||
}
|
||||
|
||||
HttpServersTable *HttpServersTable::Create()
|
||||
{
|
||||
tabWidget = new HttpServersTable();
|
||||
return tabWidget;
|
||||
}
|
||||
|
||||
void HttpServersTable::Add()
|
||||
{
|
||||
auto newServer = std::make_shared<HttpServer>();
|
||||
bool accepted = HttpServerSettingsDialog::AskForSettings(
|
||||
GetSettingsWindow(), *newServer);
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto lock = LockContext();
|
||||
GetHttpServers().emplace_back(newServer);
|
||||
}
|
||||
|
||||
HttpServerSignalManager::Instance()->Add(
|
||||
QString::fromStdString(newServer->Name()));
|
||||
}
|
||||
|
||||
void HttpServersTable::Remove()
|
||||
{
|
||||
auto selectedRows =
|
||||
tabWidget->Table()->selectionModel()->selectedRows();
|
||||
if (selectedRows.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList serverNames;
|
||||
for (const auto &row : selectedRows) {
|
||||
auto cell = tabWidget->Table()->item(row.row(), 0);
|
||||
if (!cell) {
|
||||
continue;
|
||||
}
|
||||
serverNames << cell->text();
|
||||
}
|
||||
|
||||
const int count = serverNames.size();
|
||||
if (count == 1) {
|
||||
const QString msg = obs_module_text(
|
||||
"AdvSceneSwitcher.httpServerTab.removeSingle.text");
|
||||
if (!DisplayMessage(msg.arg(serverNames.at(0)), true)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const QString msg = obs_module_text(
|
||||
"AdvSceneSwitcher.httpServerTab.removeMultiple.text");
|
||||
if (!DisplayMessage(msg.arg(count), true)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto lock = LockContext();
|
||||
RemoveItemsByName(GetHttpServers(), serverNames);
|
||||
}
|
||||
|
||||
for (const auto &name : serverNames) {
|
||||
HttpServerSignalManager::Instance()->Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList getCellLabels(HttpServer *server, bool addName = true)
|
||||
{
|
||||
assert(server);
|
||||
QStringList result;
|
||||
if (addName) {
|
||||
result << QString::fromStdString(server->Name());
|
||||
}
|
||||
result << QString::number(server->GetPort())
|
||||
<< obs_module_text(
|
||||
server->IsListening()
|
||||
? "AdvSceneSwitcher.httpServer.status.listening"
|
||||
: "AdvSceneSwitcher.httpServer.status.stopped");
|
||||
return result;
|
||||
}
|
||||
|
||||
static void updateServerStatus(QTableWidget *table)
|
||||
{
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
auto item = table->item(row, 0);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
auto weakServer = GetWeakHttpServerByQString(item->text());
|
||||
auto server = weakServer.lock();
|
||||
if (!server) {
|
||||
continue;
|
||||
}
|
||||
UpdateItemTableRow(table, row,
|
||||
getCellLabels(server.get(), false));
|
||||
}
|
||||
}
|
||||
|
||||
static void openSettingsDialog()
|
||||
{
|
||||
auto selectedRows =
|
||||
tabWidget->Table()->selectionModel()->selectedRows();
|
||||
if (selectedRows.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto cell = tabWidget->Table()->item(selectedRows.last().row(), 0);
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto weakServer = GetWeakHttpServerByQString(cell->text());
|
||||
auto server = weakServer.lock();
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto oldName = server->Name();
|
||||
bool accepted = HttpServerSettingsDialog::AskForSettings(
|
||||
GetSettingsWindow(), *server);
|
||||
if (accepted && oldName != server->Name()) {
|
||||
HttpServerSignalManager::Instance()->Rename(
|
||||
QString::fromStdString(oldName),
|
||||
QString::fromStdString(server->Name()));
|
||||
}
|
||||
updateServerStatus(tabWidget->Table());
|
||||
}
|
||||
|
||||
static const QStringList headers =
|
||||
QStringList()
|
||||
<< obs_module_text("AdvSceneSwitcher.httpServerTab.name.header")
|
||||
<< obs_module_text("AdvSceneSwitcher.httpServerTab.port.header")
|
||||
<< obs_module_text("AdvSceneSwitcher.httpServerTab.status.header");
|
||||
|
||||
HttpServersTable::HttpServersTable(QTabWidget *parent)
|
||||
: ResourceTable(
|
||||
parent,
|
||||
obs_module_text("AdvSceneSwitcher.httpServerTab.help"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.httpServerTab.addButton.tooltip"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.httpServerTab.removeButton.tooltip"),
|
||||
headers, openSettingsDialog)
|
||||
{
|
||||
for (const auto &s : GetHttpServers()) {
|
||||
auto server = std::static_pointer_cast<HttpServer>(s);
|
||||
AddItemTableRow(Table(), getCellLabels(server.get()));
|
||||
}
|
||||
SetHelpVisible(GetHttpServers().empty());
|
||||
}
|
||||
|
||||
static void setupTab(QTabWidget *tab)
|
||||
{
|
||||
if (GetHttpServers().empty()) {
|
||||
setTabVisible(tab, false);
|
||||
}
|
||||
|
||||
QWidget::connect(HttpServerSignalManager::Instance(),
|
||||
&HttpServerSignalManager::Rename, tab,
|
||||
[](const QString &oldName, const QString &newName) {
|
||||
RenameItemTableRow(tabWidget->Table(), oldName,
|
||||
newName);
|
||||
});
|
||||
QWidget::connect(
|
||||
HttpServerSignalManager::Instance(),
|
||||
&HttpServerSignalManager::Add, tab, [tab](const QString &name) {
|
||||
AddItemTableRow(tabWidget->Table(),
|
||||
getCellLabels(GetHttpServerByName(
|
||||
name.toStdString())));
|
||||
tabWidget->SetHelpVisible(false);
|
||||
tabWidget->HighlightAddButton(false);
|
||||
setTabVisible(tab, true);
|
||||
});
|
||||
QWidget::connect(HttpServerSignalManager::Instance(),
|
||||
&HttpServerSignalManager::Remove, tab,
|
||||
[](const QString &name) {
|
||||
RemoveItemTableRow(tabWidget->Table(), name);
|
||||
if (tabWidget->Table()->rowCount() == 0) {
|
||||
tabWidget->SetHelpVisible(true);
|
||||
tabWidget->HighlightAddButton(true);
|
||||
}
|
||||
});
|
||||
|
||||
auto timer = new QTimer(tabWidget);
|
||||
timer->setInterval(1000);
|
||||
QWidget::connect(timer, &QTimer::timeout,
|
||||
[]() { updateServerStatus(tabWidget->Table()); });
|
||||
timer->start();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
20
plugins/http/http-server-tab.hpp
Normal file
20
plugins/http/http-server-tab.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "resource-table.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
class HttpServersTable final : public ResourceTable {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static HttpServersTable *Create();
|
||||
|
||||
private slots:
|
||||
void Add();
|
||||
void Remove();
|
||||
|
||||
private:
|
||||
HttpServersTable(QTabWidget *parent = nullptr);
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
364
plugins/http/http-server.cpp
Normal file
364
plugins/http/http-server.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#include "http-server.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <httplib.h>
|
||||
|
||||
#undef DispatchMessage
|
||||
|
||||
Q_DECLARE_METATYPE(advss::HttpServer *);
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<std::shared_ptr<Item>> httpServers;
|
||||
static void saveHttpServers(obs_data_t *obj);
|
||||
static void loadHttpServers(obs_data_t *obj);
|
||||
static bool setup();
|
||||
static bool setupDone = setup();
|
||||
|
||||
bool setup()
|
||||
{
|
||||
AddSaveStep(saveHttpServers);
|
||||
AddLoadStep(loadHttpServers);
|
||||
AddPluginCleanupStep([]() {
|
||||
for (auto &s : httpServers) {
|
||||
auto server = std::dynamic_pointer_cast<HttpServer>(s);
|
||||
if (server) {
|
||||
server->Stop();
|
||||
}
|
||||
}
|
||||
httpServers.clear();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
static void saveHttpServers(obs_data_t *obj)
|
||||
{
|
||||
auto array = obs_data_array_create();
|
||||
for (const auto &s : httpServers) {
|
||||
auto item = obs_data_create();
|
||||
s->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
obs_data_release(item);
|
||||
}
|
||||
obs_data_set_array(obj, "httpServers", array);
|
||||
obs_data_array_release(array);
|
||||
}
|
||||
|
||||
static void loadHttpServers(obs_data_t *obj)
|
||||
{
|
||||
httpServers.clear();
|
||||
auto array = obs_data_get_array(obj, "httpServers");
|
||||
const size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto item = obs_data_array_item(array, i);
|
||||
auto server = HttpServer::Create();
|
||||
httpServers.emplace_back(server);
|
||||
httpServers.back()->Load(item);
|
||||
obs_data_release(item);
|
||||
}
|
||||
obs_data_array_release(array);
|
||||
}
|
||||
|
||||
struct HttpServer::Impl {
|
||||
std::unique_ptr<httplib::Server> server;
|
||||
std::thread thread;
|
||||
std::mutex mutex;
|
||||
std::atomic_bool listening{false};
|
||||
MessageDispatcher<HttpRequest> dispatcher;
|
||||
};
|
||||
|
||||
HttpServer::HttpServer() : _impl(std::make_unique<Impl>()) {}
|
||||
|
||||
HttpServer::HttpServer(const HttpServer &other)
|
||||
: Item(other),
|
||||
_impl(std::make_unique<Impl>())
|
||||
{
|
||||
_port = other._port;
|
||||
_startOnLoad = other._startOnLoad;
|
||||
}
|
||||
|
||||
HttpServer &HttpServer::operator=(const HttpServer &other)
|
||||
{
|
||||
if (this != &other) {
|
||||
Stop();
|
||||
_name = other._name;
|
||||
_port = other._port;
|
||||
_startOnLoad = other._startOnLoad;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool HttpServer::IsListening() const
|
||||
{
|
||||
return _impl->listening.load();
|
||||
}
|
||||
|
||||
void HttpServer::Start()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_impl->mutex);
|
||||
|
||||
if (_impl->server) {
|
||||
_impl->server->stop();
|
||||
}
|
||||
if (_impl->thread.joinable()) {
|
||||
_impl->thread.join();
|
||||
}
|
||||
|
||||
_impl->server = std::make_unique<httplib::Server>();
|
||||
_impl->listening.store(false);
|
||||
|
||||
auto handleRequest = [this](const httplib::Request &req,
|
||||
httplib::Response &res) {
|
||||
HttpRequest request;
|
||||
request.method = req.method;
|
||||
request.path = req.path;
|
||||
request.body = req.body;
|
||||
for (const auto &[name, value] : req.headers) {
|
||||
request.headers.emplace(name, value);
|
||||
}
|
||||
_impl->dispatcher.DispatchMessage(request);
|
||||
res.status = 200;
|
||||
};
|
||||
|
||||
_impl->server->Get(".*", handleRequest);
|
||||
_impl->server->Post(".*", handleRequest);
|
||||
_impl->server->Put(".*", handleRequest);
|
||||
_impl->server->Patch(".*", handleRequest);
|
||||
_impl->server->Delete(".*", handleRequest);
|
||||
|
||||
const int port = _port;
|
||||
const std::string name = _name;
|
||||
auto serverPtr = _impl->server.get();
|
||||
auto listeningFlag = &_impl->listening;
|
||||
|
||||
_impl->thread = std::thread([serverPtr, listeningFlag, port, name]() {
|
||||
if (!serverPtr->bind_to_port("0.0.0.0", port)) {
|
||||
blog(LOG_WARNING,
|
||||
"HTTP server \"%s\" failed to bind to port %d",
|
||||
name.c_str(), port);
|
||||
return;
|
||||
}
|
||||
listeningFlag->store(true);
|
||||
blog(LOG_INFO, "HTTP server \"%s\" listening on port %d",
|
||||
name.c_str(), port);
|
||||
serverPtr->listen_after_bind();
|
||||
listeningFlag->store(false);
|
||||
blog(LOG_INFO, "HTTP server \"%s\" stopped", name.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
void HttpServer::Stop()
|
||||
{
|
||||
if (!_impl) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(_impl->mutex);
|
||||
if (_impl->server) {
|
||||
_impl->server->stop();
|
||||
}
|
||||
if (_impl->thread.joinable()) {
|
||||
_impl->thread.join();
|
||||
}
|
||||
_impl->server.reset();
|
||||
_impl->listening.store(false);
|
||||
}
|
||||
|
||||
HttpRequestBuffer HttpServer::RegisterForRequests()
|
||||
{
|
||||
return _impl->dispatcher.RegisterClient();
|
||||
}
|
||||
|
||||
void HttpServer::Load(obs_data_t *obj)
|
||||
{
|
||||
Item::Load(obj);
|
||||
_port = (int)obs_data_get_int(obj, "port");
|
||||
_startOnLoad = obs_data_get_bool(obj, "startOnLoad");
|
||||
if (_startOnLoad) {
|
||||
Start();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::Save(obs_data_t *obj) const
|
||||
{
|
||||
Item::Save(obj);
|
||||
obs_data_set_int(obj, "port", _port);
|
||||
obs_data_set_bool(obj, "startOnLoad", _startOnLoad);
|
||||
}
|
||||
|
||||
HttpServer *GetHttpServerByName(const std::string &name)
|
||||
{
|
||||
for (auto &s : httpServers) {
|
||||
if (s->Name() == name) {
|
||||
return dynamic_cast<HttpServer *>(s.get());
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::weak_ptr<HttpServer> GetWeakHttpServerByName(const std::string &name)
|
||||
{
|
||||
for (const auto &s : httpServers) {
|
||||
if (s->Name() == name) {
|
||||
return std::dynamic_pointer_cast<HttpServer>(s);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &name)
|
||||
{
|
||||
return GetWeakHttpServerByName(name.toStdString());
|
||||
}
|
||||
|
||||
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &server)
|
||||
{
|
||||
auto s = server.lock();
|
||||
if (!s) {
|
||||
return obs_module_text("AdvSceneSwitcher.httpServer.invalid");
|
||||
}
|
||||
return s->Name();
|
||||
}
|
||||
|
||||
std::deque<std::shared_ptr<Item>> &GetHttpServers()
|
||||
{
|
||||
return httpServers;
|
||||
}
|
||||
|
||||
// --- HttpServerSettingsDialog ---
|
||||
|
||||
static bool ServerNameAvailable(const QString &name)
|
||||
{
|
||||
return !GetHttpServerByName(name.toStdString());
|
||||
}
|
||||
|
||||
static bool AskForSettingsWrapper(QWidget *parent, Item &settings)
|
||||
{
|
||||
HttpServer &server = dynamic_cast<HttpServer &>(settings);
|
||||
return HttpServerSettingsDialog::AskForSettings(parent, server);
|
||||
}
|
||||
|
||||
HttpServerSettingsDialog::HttpServerSettingsDialog(QWidget *parent,
|
||||
const HttpServer &settings)
|
||||
: ItemSettingsDialog(settings, httpServers,
|
||||
"AdvSceneSwitcher.httpServer.select",
|
||||
"AdvSceneSwitcher.httpServer.add",
|
||||
"AdvSceneSwitcher.item.nameNotAvailable", true,
|
||||
parent),
|
||||
_port(new QSpinBox()),
|
||||
_startOnLoad(new QCheckBox()),
|
||||
_layout(new QGridLayout())
|
||||
{
|
||||
_port->setMinimum(1);
|
||||
_port->setMaximum(65535);
|
||||
_port->setValue(settings._port);
|
||||
_startOnLoad->setChecked(settings._startOnLoad);
|
||||
|
||||
int row = 0;
|
||||
_layout->addWidget(
|
||||
new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.name")),
|
||||
row, 0);
|
||||
auto nameLayout = new QHBoxLayout;
|
||||
nameLayout->addWidget(_name);
|
||||
nameLayout->addWidget(_nameHint);
|
||||
_layout->addLayout(nameLayout, row, 1);
|
||||
++row;
|
||||
_layout->addWidget(
|
||||
new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.port")),
|
||||
row, 0);
|
||||
_layout->addWidget(_port, row, 1);
|
||||
++row;
|
||||
_layout->addWidget(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.httpServer.startOnLoad")),
|
||||
row, 0);
|
||||
_layout->addWidget(_startOnLoad, row, 1);
|
||||
++row;
|
||||
_layout->addWidget(_buttonbox, row, 0, 1, -1);
|
||||
setLayout(_layout);
|
||||
|
||||
MinimizeSizeOfColumn(_layout, 0);
|
||||
}
|
||||
|
||||
bool HttpServerSettingsDialog::AskForSettings(QWidget *parent,
|
||||
HttpServer &settings)
|
||||
{
|
||||
HttpServerSettingsDialog dialog(parent, settings);
|
||||
dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
if (dialog.exec() != DialogCode::Accepted) {
|
||||
return false;
|
||||
}
|
||||
settings._name = dialog._name->text().toStdString();
|
||||
settings._port = dialog._port->value();
|
||||
settings._startOnLoad = dialog._startOnLoad->isChecked();
|
||||
settings.Start();
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpServerSelection::HttpServerSelection(QWidget *parent)
|
||||
: ItemSelection(httpServers, HttpServer::Create, AskForSettingsWrapper,
|
||||
"AdvSceneSwitcher.httpServer.select",
|
||||
"AdvSceneSwitcher.httpServer.add",
|
||||
"AdvSceneSwitcher.item.nameNotAvailable",
|
||||
"AdvSceneSwitcher.httpServer.configure", parent)
|
||||
{
|
||||
QWidget::connect(HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Rename(const QString &, const QString &)), this,
|
||||
SLOT(RenameItem(const QString &, const QString &)));
|
||||
QWidget::connect(HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Add(const QString &)), this,
|
||||
SLOT(AddItem(const QString &)));
|
||||
QWidget::connect(HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Remove(const QString &)), this,
|
||||
SLOT(RemoveItem(const QString &)));
|
||||
|
||||
QWidget::connect(this,
|
||||
SIGNAL(ItemRenamed(const QString &, const QString &)),
|
||||
HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Rename(const QString &, const QString &)));
|
||||
QWidget::connect(this, SIGNAL(ItemAdded(const QString &)),
|
||||
HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Add(const QString &)));
|
||||
QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)),
|
||||
HttpServerSignalManager::Instance(),
|
||||
SIGNAL(Remove(const QString &)));
|
||||
}
|
||||
|
||||
void HttpServerSelection::SetServer(const std::string &name)
|
||||
{
|
||||
if (GetHttpServerByName(name)) {
|
||||
SetItem(name);
|
||||
} else {
|
||||
SetItem("");
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerSelection::SetServer(const std::weak_ptr<HttpServer> &server)
|
||||
{
|
||||
auto s = server.lock();
|
||||
if (s) {
|
||||
SetItem(s->Name());
|
||||
} else {
|
||||
SetItem("");
|
||||
}
|
||||
}
|
||||
|
||||
HttpServerSignalManager::HttpServerSignalManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
HttpServerSignalManager *HttpServerSignalManager::Instance()
|
||||
{
|
||||
static HttpServerSignalManager manager;
|
||||
return &manager;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
101
plugins/http/http-server.hpp
Normal file
101
plugins/http/http-server.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
#include "item-selection-helpers.hpp"
|
||||
#include "message-buffer.hpp"
|
||||
#include "message-dispatcher.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QGridLayout>
|
||||
#include <QSpinBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct HttpRequest {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string body;
|
||||
std::map<std::string, std::string> headers;
|
||||
};
|
||||
|
||||
using HttpRequestBuffer = std::shared_ptr<MessageBuffer<HttpRequest>>;
|
||||
|
||||
class HttpServerSelection;
|
||||
class HttpServerSettingsDialog;
|
||||
|
||||
class HttpServer : public Item {
|
||||
public:
|
||||
HttpServer();
|
||||
HttpServer(const HttpServer &);
|
||||
HttpServer &operator=(const HttpServer &);
|
||||
~HttpServer();
|
||||
|
||||
static std::shared_ptr<Item> Create()
|
||||
{
|
||||
return std::make_shared<HttpServer>();
|
||||
}
|
||||
|
||||
void Load(obs_data_t *obj);
|
||||
void Save(obs_data_t *obj) const;
|
||||
|
||||
HttpRequestBuffer RegisterForRequests();
|
||||
int GetPort() const { return _port; }
|
||||
bool IsListening() const;
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
int _port = 16384;
|
||||
bool _startOnLoad = true;
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> _impl;
|
||||
|
||||
friend HttpServerSelection;
|
||||
friend HttpServerSettingsDialog;
|
||||
};
|
||||
|
||||
class HttpServerSettingsDialog : public ItemSettingsDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HttpServerSettingsDialog(QWidget *parent, const HttpServer &);
|
||||
static bool AskForSettings(QWidget *parent, HttpServer &settings);
|
||||
|
||||
private:
|
||||
QSpinBox *_port;
|
||||
QCheckBox *_startOnLoad;
|
||||
QGridLayout *_layout;
|
||||
};
|
||||
|
||||
class HttpServerSelection : public ItemSelection {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HttpServerSelection(QWidget *parent = nullptr);
|
||||
void SetServer(const std::string &);
|
||||
void SetServer(const std::weak_ptr<HttpServer> &);
|
||||
};
|
||||
|
||||
class HttpServerSignalManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
HttpServerSignalManager(QObject *parent = nullptr);
|
||||
static HttpServerSignalManager *Instance();
|
||||
|
||||
signals:
|
||||
void Rename(const QString &, const QString &);
|
||||
void Add(const QString &);
|
||||
void Remove(const QString &);
|
||||
};
|
||||
|
||||
HttpServer *GetHttpServerByName(const std::string &);
|
||||
std::weak_ptr<HttpServer> GetWeakHttpServerByName(const std::string &);
|
||||
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &);
|
||||
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &);
|
||||
std::deque<std::shared_ptr<Item>> &GetHttpServers();
|
||||
|
||||
} // namespace advss
|
||||
@@ -112,48 +112,56 @@ static URLInfo getURLInfo(const std::string &input, bool keepParams)
|
||||
|
||||
bool MacroActionHttp::PerformAction()
|
||||
{
|
||||
// Capture all config while holding the segment lock
|
||||
const auto [host, path] = getURLInfo(_url, !_setParams);
|
||||
|
||||
httplib::Client cli(host);
|
||||
setTimeout(cli, _timeout);
|
||||
const auto params = _setParams ? getParams(_params) : httplib::Params();
|
||||
const auto headers = _setHeaders ? getHeaders(_headers)
|
||||
: httplib::Headers();
|
||||
const auto method = _method;
|
||||
const std::string body = _body;
|
||||
const std::string contentType = _contentType;
|
||||
|
||||
// Release the segment lock for the blocking network call
|
||||
httplib::Result response;
|
||||
switch (_method) {
|
||||
case MacroActionHttp::Method::GET:
|
||||
response = cli.Get(path, params, headers);
|
||||
break;
|
||||
case MacroActionHttp::Method::POST: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response =
|
||||
cli.Post(pathWithParam, headers, _body, _contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::PUT: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response = cli.Put(pathWithParam, headers, _body, _contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::PATCH: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response =
|
||||
cli.Patch(pathWithParam, headers, _body, _contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::DELETE: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response =
|
||||
cli.Delete(pathWithParam, headers, _body, _contentType);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
{
|
||||
SuspendLock suspendLock(*this);
|
||||
switch (method) {
|
||||
case MacroActionHttp::Method::GET:
|
||||
response = cli.Get(path, params, headers);
|
||||
break;
|
||||
case MacroActionHttp::Method::POST: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response = cli.Post(pathWithParam, headers, body,
|
||||
contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::PUT: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response = cli.Put(pathWithParam, headers, body,
|
||||
contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::PATCH: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response = cli.Patch(pathWithParam, headers, body,
|
||||
contentType);
|
||||
break;
|
||||
}
|
||||
case MacroActionHttp::Method::DELETE: {
|
||||
const auto pathWithParam =
|
||||
httplib::append_query_params(path, params);
|
||||
response = cli.Delete(pathWithParam, headers, body,
|
||||
contentType);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (VerboseLoggingEnabled() && !response) {
|
||||
@@ -485,10 +493,10 @@ void MacroActionHttpEdit::SetWidgetLayout()
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addLayout(actionLayout);
|
||||
layout->addWidget(_setHeaders);
|
||||
layout->addLayout(_headerListLayout);
|
||||
layout->addWidget(_setParams);
|
||||
layout->addLayout(_paramListLayout);
|
||||
layout->addWidget(_setHeaders);
|
||||
layout->addLayout(_headerListLayout);
|
||||
layout->addLayout(_contentTypeLayout);
|
||||
layout->addLayout(_bodyLayout);
|
||||
layout->addLayout(timeoutLayout);
|
||||
|
||||
313
plugins/http/macro-condition-http.cpp
Normal file
313
plugins/http/macro-condition-http.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
#include "macro-condition-http.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
|
||||
#undef DELETE
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroConditionHttp::id = "http";
|
||||
|
||||
bool MacroConditionHttp::_registered = MacroConditionFactory::Register(
|
||||
MacroConditionHttp::id,
|
||||
{MacroConditionHttp::Create, MacroConditionHttpEdit::Create,
|
||||
"AdvSceneSwitcher.condition.http"});
|
||||
|
||||
static const std::map<MacroConditionHttp::Method, std::string> methodLabels = {
|
||||
{MacroConditionHttp::Method::ANY,
|
||||
"AdvSceneSwitcher.condition.http.method.any"},
|
||||
{MacroConditionHttp::Method::GET,
|
||||
"AdvSceneSwitcher.condition.http.method.get"},
|
||||
{MacroConditionHttp::Method::POST,
|
||||
"AdvSceneSwitcher.condition.http.method.post"},
|
||||
{MacroConditionHttp::Method::PUT,
|
||||
"AdvSceneSwitcher.condition.http.method.put"},
|
||||
{MacroConditionHttp::Method::PATCH,
|
||||
"AdvSceneSwitcher.condition.http.method.patch"},
|
||||
{MacroConditionHttp::Method::DELETE,
|
||||
"AdvSceneSwitcher.condition.http.method.delete"},
|
||||
};
|
||||
|
||||
static std::string_view methodToString(MacroConditionHttp::Method method)
|
||||
{
|
||||
switch (method) {
|
||||
case MacroConditionHttp::Method::GET:
|
||||
return "GET";
|
||||
case MacroConditionHttp::Method::POST:
|
||||
return "POST";
|
||||
case MacroConditionHttp::Method::PUT:
|
||||
return "PUT";
|
||||
case MacroConditionHttp::Method::PATCH:
|
||||
return "PATCH";
|
||||
case MacroConditionHttp::Method::DELETE:
|
||||
return "DELETE";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void MacroConditionHttp::SetServer(const std::string &name)
|
||||
{
|
||||
_server = GetWeakHttpServerByName(name);
|
||||
auto server = _server.lock();
|
||||
if (!server) {
|
||||
_requestBuffer.reset();
|
||||
return;
|
||||
}
|
||||
_requestBuffer = server->RegisterForRequests();
|
||||
}
|
||||
|
||||
bool MacroConditionHttp::CheckCondition()
|
||||
{
|
||||
if (!_requestBuffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool macroWasPausedSinceLastCheck =
|
||||
MacroWasPausedSince(GetMacro(), _lastCheck);
|
||||
_lastCheck = std::chrono::high_resolution_clock::now();
|
||||
if (macroWasPausedSinceLastCheck) {
|
||||
_requestBuffer->Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!_requestBuffer->Empty()) {
|
||||
auto request = _requestBuffer->ConsumeMessage();
|
||||
if (!request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SetTempVarValue("method", request->method);
|
||||
SetTempVarValue("path", request->path);
|
||||
SetTempVarValue("body", request->body);
|
||||
|
||||
if (_method != Method::ANY &&
|
||||
request->method != methodToString(_method)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string pathPattern = std::string(_path);
|
||||
if (_pathRegex.Enabled()) {
|
||||
if (!_pathRegex.Matches(request->path, _path)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (request->path != pathPattern) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string bodyPattern = std::string(_body);
|
||||
if (_bodyRegex.Enabled()) {
|
||||
if (!_bodyRegex.Matches(request->body, _body)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (request->body != bodyPattern) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (_clearBufferOnMatch) {
|
||||
_requestBuffer->Clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroConditionHttp::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroCondition::Save(obj);
|
||||
obs_data_set_int(obj, "method", static_cast<int>(_method));
|
||||
_path.Save(obj, "path");
|
||||
_pathRegex.Save(obj, "pathRegex");
|
||||
_body.Save(obj, "body");
|
||||
_bodyRegex.Save(obj, "bodyRegex");
|
||||
obs_data_set_bool(obj, "clearBufferOnMatch", _clearBufferOnMatch);
|
||||
auto server = _server.lock();
|
||||
obs_data_set_string(obj, "server",
|
||||
server ? server->Name().c_str() : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroConditionHttp::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroCondition::Load(obj);
|
||||
_method = static_cast<Method>(obs_data_get_int(obj, "method"));
|
||||
_path.Load(obj, "path");
|
||||
_pathRegex.Load(obj, "pathRegex");
|
||||
_body.Load(obj, "body");
|
||||
_bodyRegex.Load(obj, "bodyRegex");
|
||||
_clearBufferOnMatch = obs_data_get_bool(obj, "clearBufferOnMatch");
|
||||
if (!obs_data_has_user_value(obj, "clearBufferOnMatch")) {
|
||||
_clearBufferOnMatch = true;
|
||||
}
|
||||
SetServer(obs_data_get_string(obj, "server"));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroConditionHttp::GetShortDesc() const
|
||||
{
|
||||
auto server = _server.lock();
|
||||
return server ? server->Name() : "";
|
||||
}
|
||||
|
||||
void MacroConditionHttp::SetupTempVars()
|
||||
{
|
||||
MacroCondition::SetupTempVars();
|
||||
AddTempvar("method",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.http.method"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.http.method.description"));
|
||||
AddTempvar("path",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.http.path"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.http.path.description"));
|
||||
AddTempvar("body",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.http.body"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.http.body.description"));
|
||||
}
|
||||
|
||||
static void populateMethodSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[value, name] : methodLabels) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(value));
|
||||
}
|
||||
}
|
||||
|
||||
MacroConditionHttpEdit::MacroConditionHttpEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionHttp> entryData)
|
||||
: QWidget(parent),
|
||||
_method(new QComboBox(this)),
|
||||
_server(new HttpServerSelection(this)),
|
||||
_path(new VariableLineEdit(this)),
|
||||
_pathRegex(new RegexConfigWidget(this)),
|
||||
_body(new VariableTextEdit(this)),
|
||||
_bodyRegex(new RegexConfigWidget(this)),
|
||||
_clearBufferOnMatch(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.clearBufferOnMatch")))
|
||||
{
|
||||
populateMethodSelection(_method);
|
||||
|
||||
QWidget::connect(_method, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(MethodChanged(int)));
|
||||
QWidget::connect(_server, SIGNAL(SelectionChanged(const QString &)),
|
||||
this, SLOT(ServerChanged(const QString &)));
|
||||
QWidget::connect(_path, SIGNAL(editingFinished()), this,
|
||||
SLOT(PathChanged()));
|
||||
QWidget::connect(_pathRegex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(PathRegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_body, SIGNAL(textChanged()), this,
|
||||
SLOT(BodyChanged()));
|
||||
QWidget::connect(_bodyRegex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(BodyRegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_clearBufferOnMatch, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(ClearBufferOnMatchChanged(int)));
|
||||
|
||||
auto topLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.http.layout"),
|
||||
topLayout,
|
||||
{{"{{method}}", _method}, {"{{server}}", _server}});
|
||||
|
||||
auto pathLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.http.layout.path"),
|
||||
pathLayout, {{"{{path}}", _path}, {"{{regex}}", _pathRegex}},
|
||||
false);
|
||||
|
||||
auto bodyLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.http.layout.body"),
|
||||
bodyLayout, {{"{{body}}", _body}, {"{{regex}}", _bodyRegex}},
|
||||
false);
|
||||
|
||||
auto mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(topLayout);
|
||||
mainLayout->addLayout(pathLayout);
|
||||
mainLayout->addLayout(bodyLayout);
|
||||
mainLayout->addWidget(_clearBufferOnMatch);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_method->setCurrentIndex(
|
||||
_method->findData(static_cast<int>(_entryData->_method)));
|
||||
_server->SetServer(_entryData->GetServer());
|
||||
_path->setText(_entryData->_path);
|
||||
_pathRegex->SetRegexConfig(_entryData->_pathRegex);
|
||||
_body->setPlainText(_entryData->_body);
|
||||
_bodyRegex->SetRegexConfig(_entryData->_bodyRegex);
|
||||
_clearBufferOnMatch->setChecked(_entryData->_clearBufferOnMatch);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::MethodChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_method = static_cast<MacroConditionHttp::Method>(
|
||||
_method->itemData(idx).toInt());
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::ServerChanged(const QString &name)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetServer(name.toStdString());
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::PathChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_path = _path->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::PathRegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_pathRegex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::BodyChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_body = _body->toPlainText().toUtf8().constData();
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::BodyRegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_bodyRegex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroConditionHttpEdit::ClearBufferOnMatchChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_clearBufferOnMatch = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
95
plugins/http/macro-condition-http.hpp
Normal file
95
plugins/http/macro-condition-http.hpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "http-server.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
#include "variable-text-edit.hpp"
|
||||
#include "regex-config.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroConditionHttp : public MacroCondition {
|
||||
public:
|
||||
MacroConditionHttp(Macro *m) : MacroCondition(m, true) {}
|
||||
bool CheckCondition();
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetShortDesc() const;
|
||||
std::string GetId() const { return id; }
|
||||
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroConditionHttp>(m);
|
||||
}
|
||||
|
||||
enum class Method {
|
||||
ANY = 0,
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
PATCH,
|
||||
DELETE,
|
||||
};
|
||||
|
||||
void SetServer(const std::string &name);
|
||||
std::weak_ptr<HttpServer> GetServer() const { return _server; }
|
||||
|
||||
StringVariable _path = ".*";
|
||||
StringVariable _body = ".*";
|
||||
RegexConfig _pathRegex = RegexConfig::PartialMatchRegexConfig(true);
|
||||
RegexConfig _bodyRegex = RegexConfig::PartialMatchRegexConfig(true);
|
||||
Method _method = Method::ANY;
|
||||
bool _clearBufferOnMatch = true;
|
||||
|
||||
private:
|
||||
void SetupTempVars();
|
||||
|
||||
std::weak_ptr<HttpServer> _server;
|
||||
HttpRequestBuffer _requestBuffer;
|
||||
std::chrono::high_resolution_clock::time_point _lastCheck{};
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroConditionHttpEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MacroConditionHttpEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroConditionHttp> cond = nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroCondition> cond)
|
||||
{
|
||||
return new MacroConditionHttpEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroConditionHttp>(cond));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void MethodChanged(int);
|
||||
void ServerChanged(const QString &);
|
||||
void PathChanged();
|
||||
void PathRegexChanged(const RegexConfig &);
|
||||
void BodyChanged();
|
||||
void BodyRegexChanged(const RegexConfig &);
|
||||
void ClearBufferOnMatchChanged(int);
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
private:
|
||||
QComboBox *_method;
|
||||
HttpServerSelection *_server;
|
||||
VariableLineEdit *_path;
|
||||
RegexConfigWidget *_pathRegex;
|
||||
VariableTextEdit *_body;
|
||||
RegexConfigWidget *_bodyRegex;
|
||||
QCheckBox *_clearBufferOnMatch;
|
||||
|
||||
std::shared_ptr<MacroConditionHttp> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -35,6 +35,7 @@ bool MacroActionScript::PerformAction()
|
||||
return true;
|
||||
}
|
||||
|
||||
SuspendLock suspendLock(static_cast<MacroAction &>(*this));
|
||||
(void)SendTriggerSignal();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ bool MacroConditionScript::CheckCondition()
|
||||
return false;
|
||||
}
|
||||
|
||||
SuspendLock suspendLock(static_cast<MacroCondition &>(*this));
|
||||
return SendTriggerSignal();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ MacroSegmentScriptInlineEdit::MacroSegmentScriptInlineEdit(
|
||||
_scriptType(new QComboBox(this)),
|
||||
_language(new QComboBox(this)),
|
||||
_script(new ScriptEditor(this)),
|
||||
_path(new FileSelection(FileSelection::Type::WRITE, this)),
|
||||
_path(new FileSelection(
|
||||
FileSelection::Type::WRITE, this,
|
||||
obs_module_text("AdvSceneSwitcher.script.file.select"))),
|
||||
_openFile(new QPushButton(
|
||||
obs_module_text("AdvSceneSwitcher.script.file.open"), this)),
|
||||
_fileLayout(new QHBoxLayout()),
|
||||
|
||||
@@ -68,10 +68,15 @@ void ContentClassification::SetContentClassification(
|
||||
data = obs_data_create();
|
||||
obs_data_set_array(data, "content_classification_labels", ccls);
|
||||
|
||||
const auto id = token.GetUserID();
|
||||
if (!id) {
|
||||
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||
"/helix/channels",
|
||||
{{"broadcaster_id", token.GetUserID()}},
|
||||
data.Get());
|
||||
{{"broadcaster_id", *id}}, data.Get());
|
||||
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO,
|
||||
|
||||
@@ -207,6 +207,26 @@ static obs_data_t *copyData(const OBSData &data)
|
||||
return obs_data_create_from_json(json);
|
||||
}
|
||||
|
||||
void EventSub::LogActiveSubscriptions() const
|
||||
{
|
||||
if (!VerboseLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeSubscriptions.empty()) {
|
||||
blog(LOG_INFO, "Twitch EventSub active subscriptions: none");
|
||||
return;
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "Twitch EventSub active subscriptions: %zu",
|
||||
_activeSubscriptions.size());
|
||||
for (const auto &sub : _activeSubscriptions) {
|
||||
const char *type = obs_data_get_string(sub.data, "type");
|
||||
blog(LOG_INFO, " id=%s type=%s", sub.id.c_str(),
|
||||
type ? type : "");
|
||||
}
|
||||
}
|
||||
|
||||
std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||
Subscription subscription)
|
||||
{
|
||||
@@ -218,7 +238,12 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||
|
||||
std::unique_lock<std::mutex> lock(eventSub->_subscriptionMtx);
|
||||
if (!eventSub->_connected) {
|
||||
vblog(LOG_INFO, "Twitch EventSub connect started for %s",
|
||||
if (eventSub->_reconnecting) {
|
||||
return "";
|
||||
}
|
||||
|
||||
vblog(LOG_INFO,
|
||||
"new Twitch EventSub connect attempt started for %s",
|
||||
token->GetName().c_str());
|
||||
lock.unlock();
|
||||
eventSub->Connect();
|
||||
@@ -226,9 +251,17 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||
}
|
||||
|
||||
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
|
||||
eventSub->LogActiveSubscriptions();
|
||||
return eventSub->_activeSubscriptions.find(subscription)->id;
|
||||
}
|
||||
|
||||
if (!eventSub->IsValidID(eventSub->_sessionID)) {
|
||||
vblog(LOG_INFO,
|
||||
"session ID of Twitch event sub invalid - skip %s",
|
||||
__func__);
|
||||
return "";
|
||||
}
|
||||
|
||||
OBSDataAutoRelease postData = copyData(subscription.data);
|
||||
setTransportData(postData.Get(), eventSub->_sessionID);
|
||||
auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
|
||||
@@ -236,8 +269,14 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||
postData.Get());
|
||||
|
||||
if (result.status != 202) {
|
||||
vblog(LOG_INFO, "failed to register Twitch EventSub (%d)",
|
||||
result.status);
|
||||
const char *error = obs_data_get_string(result.data, "error");
|
||||
const char *message =
|
||||
obs_data_get_string(result.data, "message");
|
||||
vblog(LOG_INFO,
|
||||
"failed to register Twitch EventSub (%d): %s - %s",
|
||||
result.status, error ? error : "no error",
|
||||
message ? message : "no message");
|
||||
eventSub->LogActiveSubscriptions();
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -246,6 +285,7 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
|
||||
subscription.id = obs_data_get_string(replyData, "id");
|
||||
eventSub->_activeSubscriptions.emplace(subscription);
|
||||
eventSub->LogActiveSubscriptions();
|
||||
return subscription.id;
|
||||
}
|
||||
|
||||
@@ -402,7 +442,9 @@ void EventSub::HandleWelcome(obs_data_t *data)
|
||||
{
|
||||
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
|
||||
_sessionID = obs_data_get_string(session, "id");
|
||||
ClearActiveSubscriptions();
|
||||
blog(LOG_INFO, "Twitch EventSub connected");
|
||||
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
|
||||
}
|
||||
|
||||
void EventSub::HandleKeepAlive() const
|
||||
@@ -430,11 +472,14 @@ void EventSub::OnServerMigrationWelcome(
|
||||
// Disable reconnect handling for old connection which will be closed
|
||||
_client->set_close_handler([](connection_hdl) {});
|
||||
_client->set_fail_handler([](connection_hdl) {});
|
||||
auto connection = _client->get_con_from_hdl(_connection);
|
||||
connection->set_close_handler([](connection_hdl) {
|
||||
vblog(LOG_INFO, "previous Twitch EventSub connection closed");
|
||||
});
|
||||
connection->set_fail_handler([](connection_hdl) {});
|
||||
if (!_connection.expired()) {
|
||||
auto connection = _client->get_con_from_hdl(_connection);
|
||||
connection->set_close_handler([](connection_hdl) {
|
||||
vblog(LOG_INFO,
|
||||
"previous Twitch EventSub connection closed");
|
||||
});
|
||||
connection->set_fail_handler([](connection_hdl) {});
|
||||
}
|
||||
|
||||
websocketpp::lib::error_code ec;
|
||||
_client->close(_connection, websocketpp::close::status::normal,
|
||||
@@ -442,6 +487,7 @@ void EventSub::OnServerMigrationWelcome(
|
||||
|
||||
_client.swap(newClient);
|
||||
_sessionID = _migrationSessionID;
|
||||
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
|
||||
_connection = newHdl;
|
||||
|
||||
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
|
||||
@@ -461,17 +507,16 @@ void EventSub::OnServerMigrationWelcome(
|
||||
|
||||
void EventSub::StartServerMigrationClient(const std::string &url)
|
||||
{
|
||||
auto client = std::make_unique<EventSubWSClient>();
|
||||
SetupClient(*client);
|
||||
_migrationClient = std::make_unique<EventSubWSClient>();
|
||||
SetupClient(*_migrationClient);
|
||||
|
||||
client->set_open_handler([this](connection_hdl) {
|
||||
_migrationClient->set_open_handler([this](connection_hdl) {
|
||||
vblog(LOG_INFO, "Twitch EventSub migration client opened");
|
||||
});
|
||||
|
||||
client->set_message_handler([this,
|
||||
&client](connection_hdl hdl,
|
||||
EventSubWSClient::message_ptr
|
||||
message) {
|
||||
_migrationClient->set_message_handler([this](connection_hdl hdl,
|
||||
EventSubWSClient::message_ptr
|
||||
message) {
|
||||
const auto msg = ParseWebSocketMessage(message);
|
||||
if (!msg) {
|
||||
return;
|
||||
@@ -487,14 +532,14 @@ void EventSub::StartServerMigrationClient(const std::string &url)
|
||||
obs_data_get_obj(data, "session");
|
||||
_migrationSessionID =
|
||||
obs_data_get_string(session, "id");
|
||||
OnServerMigrationWelcome(hdl, client);
|
||||
OnServerMigrationWelcome(hdl, _migrationClient);
|
||||
} else {
|
||||
OnMessage(hdl, message);
|
||||
}
|
||||
});
|
||||
|
||||
websocketpp::lib::error_code ec;
|
||||
auto con = client->get_connection(url, ec);
|
||||
auto con = _migrationClient->get_connection(url, ec);
|
||||
if (ec) {
|
||||
blog(LOG_ERROR,
|
||||
"Twitch EventSub migration connection failed: %s",
|
||||
@@ -504,8 +549,8 @@ void EventSub::StartServerMigrationClient(const std::string &url)
|
||||
}
|
||||
|
||||
_migrationConnection = con;
|
||||
client->connect(con);
|
||||
client->run();
|
||||
_migrationClient->connect(con);
|
||||
_migrationClient->run();
|
||||
}
|
||||
|
||||
void EventSub::HandleServerMigration(obs_data_t *data)
|
||||
@@ -563,7 +608,7 @@ void EventSub::HandleRevocation(obs_data_t *data)
|
||||
|
||||
std::lock_guard<std::mutex> lock(_subscriptionMtx);
|
||||
for (auto it = _activeSubscriptions.begin();
|
||||
it != _activeSubscriptions.begin();) {
|
||||
it != _activeSubscriptions.end();) {
|
||||
if (it->id == id) {
|
||||
it = _activeSubscriptions.erase(it);
|
||||
} else {
|
||||
|
||||
@@ -81,6 +81,8 @@ private:
|
||||
void HandleServerMigration(obs_data_t *);
|
||||
void HandleRevocation(obs_data_t *);
|
||||
|
||||
void LogActiveSubscriptions() const;
|
||||
|
||||
void RegisterInstance();
|
||||
void UnregisterInstance();
|
||||
|
||||
|
||||
@@ -189,12 +189,17 @@ void LanguageSelection::Save(obs_data_t *obj) const
|
||||
|
||||
void LanguageSelection::SetStreamLanguage(const TwitchToken &token) const
|
||||
{
|
||||
const auto id = token.GetUserID();
|
||||
if (!id) {
|
||||
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_string(data, "broadcaster_language", _language.c_str());
|
||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||
"/helix/channels",
|
||||
{{"broadcaster_id", token.GetUserID()}},
|
||||
data.Get());
|
||||
{{"broadcaster_id", *id}}, data.Get());
|
||||
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO, "Failed to set stream language! (%d)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
#include <variable-line-edit.hpp>
|
||||
#include <variable-text-edit.hpp>
|
||||
#include <duration-control.hpp>
|
||||
#include <duration.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -182,11 +183,13 @@ public:
|
||||
StringVariable _announcementMessage = obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.announcement.message");
|
||||
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
|
||||
int _nonModDelayDuration = 2;
|
||||
TwitchChannel _channel;
|
||||
StringVariable _chatMessage;
|
||||
UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN;
|
||||
StringVariable _userLogin = "user login";
|
||||
DoubleVariable _userId = 0;
|
||||
StringVariable _banReason = "";
|
||||
TwitchPointsReward _pointsReward;
|
||||
std::weak_ptr<Variable> _rewardVariable;
|
||||
bool _useVariableForRewardSelection = false;
|
||||
@@ -198,14 +201,14 @@ private:
|
||||
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
|
||||
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
|
||||
void SendChatAnnouncement(const std::shared_ptr<TwitchToken> &) const;
|
||||
void SetChatEmoteOnlyMode(const std::shared_ptr<TwitchToken> &,
|
||||
bool enable) const;
|
||||
void StartRaid(const std::shared_ptr<TwitchToken> &);
|
||||
void SendChatMessage(const std::shared_ptr<TwitchToken> &);
|
||||
void GetUserInfo(const std::shared_ptr<TwitchToken> &);
|
||||
void GetRewardInfo(const std::shared_ptr<TwitchToken> &);
|
||||
void GetChannelInfo(const std::shared_ptr<TwitchToken> &token);
|
||||
|
||||
std::optional<std::string>
|
||||
GetTargetUserID(const std::shared_ptr<TwitchToken> &) const;
|
||||
bool ResolveVariableSelectionToRewardId(
|
||||
const std::shared_ptr<TwitchToken> &);
|
||||
|
||||
@@ -251,11 +254,13 @@ private slots:
|
||||
void DurationChanged(const Duration &);
|
||||
void AnnouncementMessageChanged();
|
||||
void AnnouncementColorChanged(int index);
|
||||
void NonModDelayDurationChanged(int index);
|
||||
void ChannelChanged(const TwitchChannel &);
|
||||
void ChatMessageChanged();
|
||||
void UserInfoQueryTypeChanged(int);
|
||||
void UserLoginChanged();
|
||||
void UserIdChanged(const NumberVariable<double> &);
|
||||
void BanReasonChanged();
|
||||
void PointsRewardChanged(const TwitchPointsReward &);
|
||||
void RewardVariableChanged(const QString &);
|
||||
void ToggleRewardSelection(bool);
|
||||
@@ -274,6 +279,8 @@ private:
|
||||
void SetTokenWarning(bool visible, const QString &text = "");
|
||||
|
||||
QHBoxLayout *_layout;
|
||||
QWidget *_userModerationRow;
|
||||
QHBoxLayout *_layout2;
|
||||
FilterComboBox *_actions;
|
||||
TwitchConnectionSelection *_tokens;
|
||||
QLabel *_tokenWarning;
|
||||
@@ -288,6 +295,7 @@ private:
|
||||
DurationSelection *_duration;
|
||||
VariableTextEdit *_announcementMessage;
|
||||
QComboBox *_announcementColor;
|
||||
QComboBox *_nonModDelayDuration;
|
||||
TwitchChannelSelection *_channel;
|
||||
VariableTextEdit *_chatMessage;
|
||||
QComboBox *_userInfoQueryType;
|
||||
@@ -295,6 +303,7 @@ private:
|
||||
// QSpinBox uses int internally, which is too small for Twitch IDs, so
|
||||
// we use QDoubleSpinBox instead
|
||||
VariableDoubleSpinBox *_userId;
|
||||
VariableLineEdit *_banReason;
|
||||
TwitchPointsRewardWidget *_pointsReward;
|
||||
VariableSelection *_rewardVariable;
|
||||
QPushButton *_toggleRewardSelection;
|
||||
|
||||
@@ -1096,6 +1096,12 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
|
||||
return;
|
||||
}
|
||||
|
||||
const auto id = token->GetUserID();
|
||||
if (!id) {
|
||||
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto channelID = _channel.GetUserID(*token);
|
||||
if (!TwitchChannel::IsValid(channelID)) {
|
||||
vblog(LOG_INFO, "skip %s because of invalid channel selection",
|
||||
@@ -1114,7 +1120,7 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
|
||||
|
||||
if (includeModeratorId) {
|
||||
obs_data_set_string(condition, "moderator_user_id",
|
||||
token->GetUserID().c_str());
|
||||
id->c_str());
|
||||
}
|
||||
|
||||
obs_data_apply(condition, extraConditions);
|
||||
|
||||
@@ -26,6 +26,12 @@ void TwitchTagList::Save(obs_data_t *obj) const
|
||||
|
||||
void TwitchTagList::SetStreamTags(const TwitchToken &token) const
|
||||
{
|
||||
const auto id = token.GetUserID();
|
||||
if (!id) {
|
||||
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json j;
|
||||
j["tags"] = toVector();
|
||||
|
||||
@@ -47,8 +53,7 @@ void TwitchTagList::SetStreamTags(const TwitchToken &token) const
|
||||
|
||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||
"/helix/channels",
|
||||
{{"broadcaster_id", token.GetUserID()}},
|
||||
j.dump());
|
||||
{{"broadcaster_id", *id}}, j.dump());
|
||||
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO, "Failed to set stream tags! (%d)",
|
||||
|
||||
@@ -206,7 +206,9 @@ void TwitchToken::Save(obs_data_t *obj) const
|
||||
{
|
||||
Item::Save(obj);
|
||||
obs_data_set_string(obj, "token", _token.c_str());
|
||||
obs_data_set_string(obj, "userID", _userID.c_str());
|
||||
if (_userID) {
|
||||
obs_data_set_string(obj, "userID", _userID->c_str());
|
||||
}
|
||||
obs_data_set_bool(obj, "validateEventSubTimestamps",
|
||||
_validateEventSubTimestamps);
|
||||
obs_data_set_bool(obj, "warnIfInvalid", _warnIfInvalid);
|
||||
@@ -266,7 +268,7 @@ void TwitchToken::SetToken(const std::string &value)
|
||||
SendGetRequest(*this, "https://api.twitch.tv", "/helix/users");
|
||||
if (res.status != 200) {
|
||||
blog(LOG_WARNING, "failed to get Twitch user id from token!");
|
||||
_userID = -1;
|
||||
_userID = {};
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,12 +320,35 @@ bool TwitchToken::IsValid(bool forceUpdate) const
|
||||
cli.Get("/oauth2/validate", httplib::Params{}, headers);
|
||||
_lastValidityCheckTime = std::chrono::system_clock::now();
|
||||
_lastValidityCheckValue = _token;
|
||||
_lastValidityCheckResult = response && response->status == 200;
|
||||
if (!_lastValidityCheckResult) {
|
||||
|
||||
if (!response || response->status != 200) {
|
||||
blog(LOG_INFO, "Twitch token %s is not valid!",
|
||||
_name.c_str());
|
||||
_lastValidityCheckResult = false;
|
||||
return false;
|
||||
}
|
||||
return _lastValidityCheckResult;
|
||||
|
||||
OBSDataAutoRelease replyData =
|
||||
obs_data_create_from_json(response->body.c_str());
|
||||
const char *id = obs_data_get_string(replyData, "user_id");
|
||||
if (!id) {
|
||||
blog(LOG_INFO,
|
||||
"Twitch token %s does validity check did not report user_id! Assume invalid!",
|
||||
_name.c_str());
|
||||
_lastValidityCheckResult = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_userID && _userID != id) {
|
||||
blog(LOG_INFO,
|
||||
"Twitch token %s does not match expected user (got %s, expected %s)!",
|
||||
_name.c_str(), id, _userID->c_str());
|
||||
_lastValidityCheckResult = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastValidityCheckResult = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
const bool tokenChanged = _lastValidityCheckValue != _token;
|
||||
@@ -729,6 +754,7 @@ void TwitchTokenSettingsDialog::RequestToken()
|
||||
|
||||
auto scope = QString::fromStdString(
|
||||
generateScopeString(GetEnabledOptions()));
|
||||
_validationTimer.stop();
|
||||
_tokenGrabber.SetTokenScope(scope);
|
||||
_tokenGrabber.start();
|
||||
_tokenStatus->setText(obs_module_text(
|
||||
@@ -766,6 +792,7 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
|
||||
Q_ARG(const QString &, name));
|
||||
SetTokenInfoVisible(true);
|
||||
_requestToken->setEnabled(true);
|
||||
_validationTimer.start();
|
||||
}
|
||||
|
||||
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
void SetToken(const std::string &);
|
||||
bool IsEmpty() const { return _token.empty(); }
|
||||
std::optional<std::string> GetToken() const;
|
||||
std::string GetUserID() const { return _userID; }
|
||||
std::optional<std::string> GetUserID() const { return _userID; }
|
||||
std::shared_ptr<EventSub> GetEventSub();
|
||||
bool ValidateTimestamps() const { return _validateEventSubTimestamps; }
|
||||
bool IsValid(bool forceUpdate = false) const;
|
||||
@@ -65,7 +65,7 @@ private:
|
||||
mutable std::string _lastValidityCheckValue;
|
||||
mutable bool _lastValidityCheckResult = false;
|
||||
mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
|
||||
std::string _userID;
|
||||
std::optional<std::string> _userID;
|
||||
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
|
||||
std::shared_ptr<EventSub> _eventSub;
|
||||
bool _validateEventSubTimestamps = false;
|
||||
|
||||
@@ -185,6 +185,11 @@ static RequestResult processResult(const httplib::Result &response,
|
||||
return result;
|
||||
}
|
||||
|
||||
if (response->status < 200 || response->status >= 300) {
|
||||
vblog(LOG_INFO, "Twitch API error response: %s",
|
||||
response->body.c_str());
|
||||
}
|
||||
|
||||
OBSDataAutoRelease replyData =
|
||||
obs_data_create_from_json(response->body.c_str());
|
||||
result.data = replyData;
|
||||
@@ -518,6 +523,13 @@ static RequestResult sendDeleteRequest(const TwitchToken &token,
|
||||
return processResult(response, __func__);
|
||||
}
|
||||
|
||||
RequestResult SendDeleteRequest(const TwitchToken &token,
|
||||
const std::string &uri, const std::string &path,
|
||||
const httplib::Params ¶ms)
|
||||
{
|
||||
return sendDeleteRequest(token, uri, path, params);
|
||||
}
|
||||
|
||||
void SetJsonTempVars(const std::string &jsonStr,
|
||||
std::function<void(const char *, const char *)> setVarFunc)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,9 @@ RequestResult SendPatchRequest(const TwitchToken &token, const std::string &uri,
|
||||
const httplib::Params ¶ms = {},
|
||||
const std::string &data = "",
|
||||
bool useCache = false);
|
||||
RequestResult SendDeleteRequest(const TwitchToken &token,
|
||||
const std::string &uri, const std::string &path,
|
||||
const httplib::Params ¶ms = {});
|
||||
|
||||
// Helper functions to set temp var values
|
||||
|
||||
|
||||
@@ -153,6 +153,25 @@ static QStringList getCellLabels(TwitchToken *token, bool addName = true)
|
||||
return result;
|
||||
}
|
||||
|
||||
static void updateConnectionStatus(QTableWidget *table)
|
||||
{
|
||||
for (int row = 0; row < table->rowCount(); row++) {
|
||||
auto item = table->item(row, 0);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto weakToken = GetWeakTwitchTokenByQString(item->text());
|
||||
auto token = weakToken.lock();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UpdateItemTableRow(table, row,
|
||||
getCellLabels(token.get(), false));
|
||||
}
|
||||
}
|
||||
|
||||
static void openSettingsDialog()
|
||||
{
|
||||
auto selectedRows =
|
||||
@@ -174,6 +193,7 @@ static void openSettingsDialog()
|
||||
|
||||
TwitchTokenSettingsDialog::AskForSettings(GetSettingsWindow(),
|
||||
*token.get());
|
||||
updateConnectionStatus(tabWidget->Table());
|
||||
}
|
||||
|
||||
static const QStringList headers =
|
||||
@@ -202,25 +222,6 @@ TwitchConnectionsTable::TwitchConnectionsTable(QTabWidget *parent)
|
||||
SetHelpVisible(GetTwitchTokens().empty());
|
||||
}
|
||||
|
||||
static void updateConnectionStatus(QTableWidget *table)
|
||||
{
|
||||
for (int row = 0; row < table->rowCount(); row++) {
|
||||
auto item = table->item(row, 0);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto weakToken = GetWeakTwitchTokenByQString(item->text());
|
||||
auto token = weakToken.lock();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UpdateItemTableRow(table, row,
|
||||
getCellLabels(token.get(), false));
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList getInvalidTokens()
|
||||
{
|
||||
QStringList tokens;
|
||||
|
||||
@@ -283,10 +283,18 @@ bool MacroConditionVideo::ScreenshotContainsPattern()
|
||||
SetTempVarValue("patternCount", "0");
|
||||
return false;
|
||||
}
|
||||
const auto count = countNonZero(result);
|
||||
|
||||
SetTempVarValue("similarity", std::to_string(bestMatchValue));
|
||||
SetTempVarValue("patternCount", std::to_string(count));
|
||||
return count > 0;
|
||||
|
||||
if (IsTempVarInUse("patternCount")) {
|
||||
const auto count = CountPatternMatches(
|
||||
result, {_patternImageData.rgbaPattern.cols,
|
||||
_patternImageData.rgbaPattern.rows});
|
||||
SetTempVarValue("patternCount", std::to_string(count));
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
return countNonZero(result) > 0;
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::FileInputIsUpToDate() const
|
||||
@@ -369,13 +377,19 @@ bool MacroConditionVideo::CheckColor()
|
||||
_screenshotData.GetImage(), _colorParameters.color,
|
||||
_colorParameters.colorThreshold,
|
||||
_colorParameters.matchThreshold);
|
||||
// Way too slow for now
|
||||
//SetTempVarValue("dominantColor", GetDominantColor(_screenshotData.image, 3)
|
||||
// .name(QColor::HexArgb)
|
||||
// .toStdString());
|
||||
SetTempVarValue("color", GetAverageColor(_screenshotData.GetImage())
|
||||
.name(QColor::HexArgb)
|
||||
.toStdString());
|
||||
|
||||
SetTempVarValue("color", [&]() {
|
||||
return GetAverageColor(_screenshotData.GetImage())
|
||||
.name(QColor::HexArgb)
|
||||
.toStdString();
|
||||
});
|
||||
|
||||
SetTempVarValue("dominantColor", [&]() {
|
||||
return GetDominantColor(_screenshotData.GetImage(), 3)
|
||||
.name(QColor::HexArgb)
|
||||
.toStdString();
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -471,6 +485,12 @@ void MacroConditionVideo::SetupTempVars()
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.video.color"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.video.color.description"));
|
||||
AddTempvar(
|
||||
"dominantColor",
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.video.dominantColor"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.video.dominantColor.description"));
|
||||
break;
|
||||
case VideoCondition::MATCH:
|
||||
case VideoCondition::DIFFER:
|
||||
|
||||
@@ -97,6 +97,35 @@ double MatchPattern(QImage &img, const PatternImageData &patternData,
|
||||
return bestFitValue;
|
||||
}
|
||||
|
||||
int CountPatternMatches(const cv::Mat &result, const cv::Size &patternSize)
|
||||
{
|
||||
if (result.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cv::Mat work = result.clone();
|
||||
int count = 0;
|
||||
|
||||
while (true) {
|
||||
double maxVal;
|
||||
cv::Point maxLoc;
|
||||
cv::minMaxLoc(work, nullptr, &maxVal, nullptr, &maxLoc);
|
||||
if (maxVal <= 0.0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
// Suppress the template-sized region around this match so
|
||||
// overlapping high-scoring positions are not counted separately
|
||||
int x = std::max(0, maxLoc.x - patternSize.width / 2);
|
||||
int y = std::max(0, maxLoc.y - patternSize.height / 2);
|
||||
int w = std::min(patternSize.width, work.cols - x);
|
||||
int h = std::min(patternSize.height, work.rows - y);
|
||||
work(cv::Rect(x, y, w, h)) = 0.0f;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
double MatchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchColor)
|
||||
@@ -257,9 +286,9 @@ QColor GetAverageColor(const QImage &img)
|
||||
|
||||
auto image = QImageToMat(img);
|
||||
cv::Scalar meanColor = cv::mean(image);
|
||||
int averageBlue = cvRound(meanColor[0]);
|
||||
int averageRed = cvRound(meanColor[0]);
|
||||
int averageGreen = cvRound(meanColor[1]);
|
||||
int averageRed = cvRound(meanColor[2]);
|
||||
int averageBlue = cvRound(meanColor[2]);
|
||||
|
||||
return QColor(averageRed, averageGreen, averageBlue);
|
||||
}
|
||||
@@ -280,8 +309,8 @@ QColor GetDominantColor(const QImage &img, int k)
|
||||
cv::TermCriteria criteria(
|
||||
cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 100, 0.2);
|
||||
cv::Mat labels, centers;
|
||||
cv::kmeans(reshapedImage, k, labels, criteria, 1,
|
||||
cv::KMEANS_RANDOM_CENTERS, centers);
|
||||
cv::kmeans(reshapedImage, k, labels, criteria, 3, cv::KMEANS_PP_CENTERS,
|
||||
centers);
|
||||
|
||||
// Find the dominant color
|
||||
// Center of the cluster with the largest number of pixels
|
||||
@@ -292,16 +321,16 @@ QColor GetDominantColor(const QImage &img, int k)
|
||||
|
||||
cv::Point max_loc;
|
||||
cv::minMaxLoc(counts, nullptr, nullptr, nullptr, &max_loc);
|
||||
try {
|
||||
cv::Scalar dominantColor = centers.at<cv::Scalar>(max_loc.y);
|
||||
const int blue = cv::saturate_cast<int>(dominantColor.val[0]);
|
||||
const int green = cv::saturate_cast<int>(dominantColor.val[1]);
|
||||
const int red = cv::saturate_cast<int>(dominantColor.val[2]);
|
||||
const int alpha = cv::saturate_cast<int>(dominantColor.val[3]);
|
||||
return QColor(red, green, blue, alpha);
|
||||
} catch (...) {
|
||||
}
|
||||
return QColor();
|
||||
const int clusterIdx = max_loc.x;
|
||||
const int red =
|
||||
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 0));
|
||||
const int green =
|
||||
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 1));
|
||||
const int blue =
|
||||
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 2));
|
||||
const int alpha =
|
||||
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 3));
|
||||
return QColor(red, green, blue, alpha);
|
||||
}
|
||||
|
||||
// Assumption is that QImage uses Format_RGBA8888.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user