mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-09-06 16:46:11 -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
|
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).
|
More information can be found [here](https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
@@ -145,11 +145,7 @@ AdvSceneSwitcher.condition.file="Datei"
|
|||||||
AdvSceneSwitcher.condition.file.type.match="entspricht"
|
AdvSceneSwitcher.condition.file.type.match="entspricht"
|
||||||
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
|
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
|
||||||
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
|
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
|
||||||
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
|
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||||
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.media="Medien"
|
AdvSceneSwitcher.condition.media="Medien"
|
||||||
AdvSceneSwitcher.condition.media.source="Quelle"
|
AdvSceneSwitcher.condition.media.source="Quelle"
|
||||||
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
|
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.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.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.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="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.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"
|
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.sceneOverride="Szenenübergang überschreiben"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
|
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
|
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.layout.type="Anpassen {{type}}{{scenes}}{{sources}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
|
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
|
||||||
AdvSceneSwitcher.action.timer="Timer"
|
AdvSceneSwitcher.action.timer="Timer"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
|
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"
|
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.disableUIHints="Disable UI hints"
|
||||||
AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus"
|
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.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.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.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"
|
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.exists="exists"
|
||||||
AdvSceneSwitcher.condition.file.type.isFile="is a file"
|
AdvSceneSwitcher.condition.file.type.isFile="is a file"
|
||||||
AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
|
AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
|
||||||
AdvSceneSwitcher.condition.file.remote="Remote file"
|
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||||
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.media="Media"
|
AdvSceneSwitcher.condition.media="Media"
|
||||||
AdvSceneSwitcher.condition.media.checkType.state="State matches"
|
AdvSceneSwitcher.condition.media.checkType.state="State matches"
|
||||||
AdvSceneSwitcher.condition.media.checkType.time="Time restriction 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.useRegex="Use regular expressions"
|
||||||
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
|
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
|
||||||
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
|
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.temporaryVariable="Macro property"
|
||||||
AdvSceneSwitcher.condition.variable="Variable"
|
AdvSceneSwitcher.condition.variable="Variable"
|
||||||
AdvSceneSwitcher.condition.variable.type.compare="equals"
|
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.wait="Wait for fade to complete."
|
||||||
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
|
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
|
||||||
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}{{track}}"
|
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="Recording"
|
||||||
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
|
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
|
||||||
AdvSceneSwitcher.action.recording.type.start="Start 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.split="Split recording file"
|
||||||
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
|
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
|
||||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename formatting"
|
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.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.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="Replay buffer"
|
||||||
AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!"
|
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!"
|
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.closeInteractionDialog="Close interaction dialog"
|
||||||
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
|
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
|
||||||
AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties 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="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}"
|
||||||
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
|
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\""
|
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="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.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.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="Media"
|
||||||
AdvSceneSwitcher.action.media.type.play="Play"
|
AdvSceneSwitcher.action.media.type.play="Play"
|
||||||
AdvSceneSwitcher.action.media.type.pause="Pause"
|
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.disableAction="Disable action"
|
||||||
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
|
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
|
||||||
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle 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.type.nestedMacro="Nested macro"
|
||||||
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
|
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
|
||||||
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
|
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.type.disable="Disable studio mode"
|
||||||
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
|
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
|
||||||
AdvSceneSwitcher.action.transition="Transition"
|
AdvSceneSwitcher.action.transition="Transition"
|
||||||
AdvSceneSwitcher.action.transition.type.scene="scene transition"
|
AdvSceneSwitcher.action.transition.type.scene="Modify scene transition"
|
||||||
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override"
|
AdvSceneSwitcher.action.transition.type.sceneOverride="Modify scene transition override"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition"
|
AdvSceneSwitcher.action.transition.type.sourceShow="Modify source show transition"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition"
|
AdvSceneSwitcher.action.transition.type.sourceHide="Modify source hide transition"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.type.tbar="Modify T-Bar position"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}"
|
AdvSceneSwitcher.action.transition.type.releaseTbar="Release T-Bar"
|
||||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds"
|
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="Timer"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Pause"
|
AdvSceneSwitcher.action.timer.type.pause="Pause"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Continue"
|
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.tags.set="Set stream tags"
|
||||||
AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language"
|
AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language"
|
||||||
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
|
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.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.marker.create="Create stream marker"
|
||||||
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
|
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
|
||||||
AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement"
|
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.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.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.chat.sendMessage="Send chat message"
|
||||||
AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information"
|
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.reward.getInfo="Get channel points reward information"
|
||||||
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
|
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
|
||||||
AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control"
|
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.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.chat="Using account{{account}}{{actions}}on{{channel}}"
|
||||||
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="Using account{{account}}{{actions}}of{{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.user.getInfo.row1="Using account{{account}}{{actions}}"
|
||||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
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.title.title="Enter title"
|
||||||
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
|
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
|
||||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
|
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="Tag Limit Reached"
|
||||||
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
|
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.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.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.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."
|
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.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="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.close="Close"
|
||||||
AdvSceneSwitcher.browse="Browse"
|
AdvSceneSwitcher.browse="Browse"
|
||||||
@@ -1664,6 +1780,7 @@ AdvSceneSwitcher.script.language.lua="LUA"
|
|||||||
AdvSceneSwitcher.script.language.select="--select language--"
|
AdvSceneSwitcher.script.language.select="--select language--"
|
||||||
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
|
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
|
||||||
AdvSceneSwitcher.script.file.open="Open"
|
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.open.failed="Could not open script file!"
|
||||||
AdvSceneSwitcher.script.file.warning.notFound="Warning: The given file path was not found!"
|
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!"
|
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.runCount.description="The number of times a macro was executed."
|
||||||
AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count"
|
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.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"
|
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.text.description="The text detected in a given video input frame."
|
||||||
AdvSceneSwitcher.tempVar.video.color="Average color"
|
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.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="Received websocket message"
|
||||||
AdvSceneSwitcher.tempVar.websocket.message.description="The received websocket message, which matched the given pattern"
|
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.gameCapture.executable.description="Executable name of the application captured by the source."
|
||||||
|
|
||||||
AdvSceneSwitcher.tempVar.http.status="Status code"
|
AdvSceneSwitcher.tempVar.http.status="Status code"
|
||||||
AdvSceneSwitcher.tempVar.http.body="Message body"
|
|
||||||
AdvSceneSwitcher.tempVar.http.error="Error"
|
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.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"
|
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="Last save path"
|
||||||
AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved."
|
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.selectScene="--select scene--"
|
||||||
AdvSceneSwitcher.selectCanvas="--select canvas--"
|
AdvSceneSwitcher.selectCanvas="--select canvas--"
|
||||||
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
|
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.scene.previousSceneTransitionBehaviour="Durante la transición, verifique la escena de origen de la transición"
|
||||||
AdvSceneSwitcher.condition.window="Ventana"
|
AdvSceneSwitcher.condition.window="Ventana"
|
||||||
AdvSceneSwitcher.condition.file="Archivo"
|
AdvSceneSwitcher.condition.file="Archivo"
|
||||||
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
AdvSceneSwitcher.condition.file.layout="Contenido de{{filePath}}{{conditions}}{{regex}}"
|
||||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
|
||||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
|
||||||
AdvSceneSwitcher.condition.media="Medios"
|
AdvSceneSwitcher.condition.media="Medios"
|
||||||
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
|
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
|
||||||
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
|
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.pause="Pausar grabación"
|
||||||
AdvSceneSwitcher.action.recording.type.unpause="Reanudar 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.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="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.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"
|
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.sceneOverride="anulación de transición de escena"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen"
|
AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
|
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="Modificar {{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.layout.type="Modificar {{type}}{{scenes}}{{sources}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{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.duration="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
|
||||||
AdvSceneSwitcher.action.timer="Temporizador"
|
AdvSceneSwitcher.action.timer="Temporizador"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Pausa"
|
AdvSceneSwitcher.action.timer.type.pause="Pausa"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Continuar"
|
AdvSceneSwitcher.action.timer.type.continue="Continuar"
|
||||||
|
|||||||
@@ -187,8 +187,6 @@ AdvSceneSwitcher.condition.file="Fichier"
|
|||||||
AdvSceneSwitcher.condition.file.type.match="correspond à"
|
AdvSceneSwitcher.condition.file.type.match="correspond à"
|
||||||
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
|
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
|
||||||
AdvSceneSwitcher.condition.file.type.dateChange="a changé de date de modification"
|
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="Média"
|
||||||
AdvSceneSwitcher.condition.media.source="Source"
|
AdvSceneSwitcher.condition.media.source="Source"
|
||||||
AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène"
|
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.sceneOverride="Remplacement de la transition de scène"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source"
|
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.type.sourceHide="Transition de masquage de la source"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="Modifier{{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.layout.type="Modifier{{type}}{{scenes}}{{sources}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Définir le type de transition sur{{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{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.duration="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
|
||||||
AdvSceneSwitcher.action.timer="Minuterie"
|
AdvSceneSwitcher.action.timer="Minuterie"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
|
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Continuer"
|
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.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。"
|
||||||
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
|
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
|
||||||
AdvSceneSwitcher.condition.file="ファイル"
|
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="Media"
|
||||||
AdvSceneSwitcher.condition.media.checkType.state="状態が一致"
|
AdvSceneSwitcher.condition.media.checkType.state="状態が一致"
|
||||||
AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
|
AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
|
||||||
@@ -1020,9 +1012,9 @@ AdvSceneSwitcher.action.transition="トランジション"
|
|||||||
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
|
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
|
||||||
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
|
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
|
||||||
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
|
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
|
||||||
; AdvSceneSwitcher.action.transition.entry.line1="{{type}}{{scenes}}{{sources}} の変更"
|
; AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}} の変更"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
|
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
|
||||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}移行時間を {{duration}} 秒に設定します"
|
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}移行時間を {{duration}} 秒に設定します"
|
||||||
AdvSceneSwitcher.action.timer="タイマー"
|
AdvSceneSwitcher.action.timer="タイマー"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="一時停止"
|
AdvSceneSwitcher.action.timer.type.pause="一時停止"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="続行"
|
AdvSceneSwitcher.action.timer.type.continue="続行"
|
||||||
@@ -1190,8 +1182,15 @@ AdvSceneSwitcher.action.twitch.reward.toggleControl="リワード名/変数選
|
|||||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません!"
|
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません!"
|
||||||
; AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
|
; 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.chat="{{channel}}でアカウント{{account}}{{actions}}を使用しています"
|
||||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo="{{userInfoQueryType}}{{userLogin}}{{userId}}でアカウント{{account}}{{actions}}を使用"
|
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
|
||||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="チャンネル{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}でアカウント{{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.title.title="タイトルを入力"
|
||||||
AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明"
|
AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明"
|
||||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"
|
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"
|
||||||
|
|||||||
@@ -295,11 +295,6 @@ AdvSceneSwitcher.condition.file="Arquivo"
|
|||||||
AdvSceneSwitcher.condition.file.type.match="corresponde"
|
AdvSceneSwitcher.condition.file.type.match="corresponde"
|
||||||
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou"
|
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou"
|
||||||
AdvSceneSwitcher.condition.file.type.dateChange="data de modificação 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="Mídia"
|
||||||
AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde"
|
AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde"
|
||||||
AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo 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.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.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.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="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.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!"
|
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.sceneOverride="substituição de transição de cena"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="transição de exibição de fonte"
|
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.type.sourceHide="transição de ocultação de fonte"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="Modificar{{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.layout.type="Modificar{{type}}{{scenes}}{{sources}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Definir tipo de transição para{{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{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.duration="{{setDuration}}Definir duração da transição para{{duration}}segundos"
|
||||||
AdvSceneSwitcher.action.timer="Temporizador"
|
AdvSceneSwitcher.action.timer="Temporizador"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Pausar"
|
AdvSceneSwitcher.action.timer.type.pause="Pausar"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Continuar"
|
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.emoteOnly.disable="Desabilitar modo apenas emotes no chat"
|
||||||
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de 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.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.layout.chat="Usando conta{{account}}{{actions}}em{{channel}}"
|
||||||
AdvSceneSwitcher.action.twitch.title.title="Digite o título"
|
AdvSceneSwitcher.action.twitch.title.title="Digite o título"
|
||||||
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"
|
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
|
|||||||
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
|
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
|
||||||
AdvSceneSwitcher.condition.window="Окно"
|
AdvSceneSwitcher.condition.window="Окно"
|
||||||
AdvSceneSwitcher.condition.file="Файл"
|
AdvSceneSwitcher.condition.file="Файл"
|
||||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
|
||||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
|
||||||
AdvSceneSwitcher.condition.media="Медиа"
|
AdvSceneSwitcher.condition.media="Медиа"
|
||||||
AdvSceneSwitcher.condition.video="Видео"
|
AdvSceneSwitcher.condition.video="Видео"
|
||||||
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
|
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
|
||||||
@@ -130,7 +128,6 @@ AdvSceneSwitcher.action.recording.type.start="Начать запись"
|
|||||||
AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
|
AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
|
||||||
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
|
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
|
||||||
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
|
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
|
||||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
|
||||||
AdvSceneSwitcher.action.replay="Буфер воспроизведения"
|
AdvSceneSwitcher.action.replay="Буфер воспроизведения"
|
||||||
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
|
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
|
||||||
AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера"
|
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.scene.currentSceneTransitionBehaviour="Geçiş hedefi sahnesi için geçiş kontrolü sırasında"
|
||||||
AdvSceneSwitcher.condition.window="Pencere"
|
AdvSceneSwitcher.condition.window="Pencere"
|
||||||
AdvSceneSwitcher.condition.file="Dosya"
|
AdvSceneSwitcher.condition.file="Dosya"
|
||||||
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
|
AdvSceneSwitcher.condition.file.layout="İçerik{{filePath}}{{conditions}}{{regex}}"
|
||||||
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
|
|
||||||
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
|
|
||||||
AdvSceneSwitcher.condition.media="Medya"
|
AdvSceneSwitcher.condition.media="Medya"
|
||||||
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
|
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
|
||||||
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
|
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.pause="Kayıt Duraklat"
|
||||||
AdvSceneSwitcher.action.recording.type.unpause="Kayıt Duraklatma"
|
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.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="Tekrar arabelleği"
|
||||||
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
|
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
|
||||||
AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat"
|
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.type.append="Ekle"
|
||||||
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
|
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
|
||||||
AdvSceneSwitcher.action.transition="Geçiş"
|
AdvSceneSwitcher.action.transition="Geçiş"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{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.duration="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
|
||||||
AdvSceneSwitcher.action.timer="Zamanlayıcı"
|
AdvSceneSwitcher.action.timer="Zamanlayıcı"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="Duraklat"
|
AdvSceneSwitcher.action.timer.type.pause="Duraklat"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="Devam et"
|
AdvSceneSwitcher.action.timer.type.continue="Devam et"
|
||||||
|
|||||||
@@ -318,11 +318,7 @@ AdvSceneSwitcher.condition.file="文件"
|
|||||||
AdvSceneSwitcher.condition.file.type.match="内容匹配"
|
AdvSceneSwitcher.condition.file.type.match="内容匹配"
|
||||||
AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变"
|
AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变"
|
||||||
AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变"
|
AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变"
|
||||||
AdvSceneSwitcher.condition.file.remote="网络文件"
|
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
|
||||||
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="媒体"
|
AdvSceneSwitcher.condition.media="媒体"
|
||||||
AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
|
AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
|
||||||
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
|
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
|
||||||
@@ -813,7 +809,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="更改输出文件夹
|
|||||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
|
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
|
||||||
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
|
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
|
||||||
AdvSceneSwitcher.action.recording.split.hint="注意,请先确保在OBS设置中启用自动分割文件!"
|
AdvSceneSwitcher.action.recording.split.hint="注意,请先确保在OBS设置中启用自动分割文件!"
|
||||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
|
||||||
AdvSceneSwitcher.action.replay="回放缓存"
|
AdvSceneSwitcher.action.replay="回放缓存"
|
||||||
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
|
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
|
||||||
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
|
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
|
||||||
@@ -994,9 +989,9 @@ AdvSceneSwitcher.action.transition.type.scene="场景转场动画"
|
|||||||
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
|
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
|
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
|
||||||
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
|
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
|
||||||
AdvSceneSwitcher.action.transition.entry.line1="更改 {{type}}{{scenes}}{{sources}}"
|
AdvSceneSwitcher.action.transition.layout.type="更改 {{type}}{{scenes}}{{sources}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}将转场动画类型设置为 {{transitions}}"
|
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}将转场动画类型设置为 {{transitions}}"
|
||||||
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
|
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
|
||||||
AdvSceneSwitcher.action.timer="计时器"
|
AdvSceneSwitcher.action.timer="计时器"
|
||||||
AdvSceneSwitcher.action.timer.type.pause="暂停"
|
AdvSceneSwitcher.action.timer.type.pause="暂停"
|
||||||
AdvSceneSwitcher.action.timer.type.continue="继续"
|
AdvSceneSwitcher.action.timer.type.continue="继续"
|
||||||
@@ -1139,10 +1134,17 @@ AdvSceneSwitcher.action.twitch.type.user.getInfo="获取用户信息"
|
|||||||
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
|
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
|
||||||
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
|
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
|
||||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="在未选择 Twitch 帐户的情况下无法选择类别!"
|
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.chat="使用账户{{account}}{{actions}}{{channel}}"
|
||||||
AdvSceneSwitcher.action.twitch.layout.user.getInfo="使用账户{{account}}{{actions}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
|
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="使用账户{{account}}{{actions}}频道{{channel}}"
|
||||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="使用账户{{account}}{{actions}} 频道{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
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.title.title="输入标题"
|
||||||
AdvSceneSwitcher.action.twitch.marker.description="标记描述"
|
AdvSceneSwitcher.action.twitch.marker.description="标记描述"
|
||||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"
|
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"
|
||||||
|
|||||||
@@ -251,6 +251,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="suppressCrashRecoveryDialog">
|
||||||
|
<property name="text">
|
||||||
|
<string>AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -597,7 +604,7 @@
|
|||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QGroupBox" name="groupBox_5">
|
<widget class="QGroupBox" name="macroListBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>AdvSceneSwitcher.macroTab.macros</string>
|
<string>AdvSceneSwitcher.macroTab.macros</string>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public slots:
|
|||||||
void on_uiHintsDisable_stateChanged(int state);
|
void on_uiHintsDisable_stateChanged(int state);
|
||||||
void on_disableComboBoxFilter_stateChanged(int state);
|
void on_disableComboBoxFilter_stateChanged(int state);
|
||||||
void on_warnPluginLoadFailure_stateChanged(int state);
|
void on_warnPluginLoadFailure_stateChanged(int state);
|
||||||
|
void on_suppressCrashRecoveryDialog_stateChanged(int state);
|
||||||
void on_hideLegacyTabs_stateChanged(int state);
|
void on_hideLegacyTabs_stateChanged(int state);
|
||||||
void on_priorityUp_clicked();
|
void on_priorityUp_clicked();
|
||||||
void on_priorityDown_clicked();
|
void on_priorityDown_clicked();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "advanced-scene-switcher.hpp"
|
#include "advanced-scene-switcher.hpp"
|
||||||
|
#include "crash-handler.hpp"
|
||||||
#include "file-selection.hpp"
|
#include "file-selection.hpp"
|
||||||
#include "filter-combo-box.hpp"
|
#include "filter-combo-box.hpp"
|
||||||
#include "first-run-wizard.hpp"
|
#include "first-run-wizard.hpp"
|
||||||
@@ -194,6 +195,15 @@ void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
|
|||||||
switcher->warnPluginLoadFailure = state;
|
switcher->warnPluginLoadFailure = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AdvSceneSwitcher::on_suppressCrashRecoveryDialog_stateChanged(int state)
|
||||||
|
{
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSuppressCrashDialog(state);
|
||||||
|
}
|
||||||
|
|
||||||
static bool isLegacyTab(const QString &name)
|
static bool isLegacyTab(const QString &name)
|
||||||
{
|
{
|
||||||
return name == obs_module_text(
|
return name == obs_module_text(
|
||||||
@@ -946,6 +956,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
|
|||||||
FilterComboBox::SetFilterBehaviourEnabled(
|
FilterComboBox::SetFilterBehaviourEnabled(
|
||||||
!switcher->disableFilterComboboxFilter);
|
!switcher->disableFilterComboboxFilter);
|
||||||
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
||||||
|
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
|
||||||
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
||||||
|
|
||||||
populatePriorityFunctionList(ui->priorityList);
|
populatePriorityFunctionList(ui->priorityList);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "macro.hpp"
|
#include "macro.hpp"
|
||||||
#include "macro-action-factory.hpp"
|
#include "macro-action-factory.hpp"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
const std::string MacroActionMacro::id = "macro";
|
const std::string MacroActionMacro::id = "macro";
|
||||||
@@ -118,6 +120,36 @@ bool MacroActionMacro::PerformAction()
|
|||||||
case Action::TOGGLE_ACTION:
|
case Action::TOGGLE_ACTION:
|
||||||
AdjustActionState(macro);
|
AdjustActionState(macro);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -175,6 +207,9 @@ void MacroActionMacro::LogAction() const
|
|||||||
case Action::NESTED_MACRO:
|
case Action::NESTED_MACRO:
|
||||||
ablog(LOG_INFO, "run nested macro");
|
ablog(LOG_INFO, "run nested macro");
|
||||||
break;
|
break;
|
||||||
|
case Action::GET_INFO:
|
||||||
|
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -202,8 +237,8 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
|
|||||||
bool MacroActionMacro::Load(obs_data_t *obj)
|
bool MacroActionMacro::Load(obs_data_t *obj)
|
||||||
{
|
{
|
||||||
MacroAction::Load(obj);
|
MacroAction::Load(obj);
|
||||||
_action = static_cast<MacroActionMacro::Action>(
|
SetAction(static_cast<MacroActionMacro::Action>(
|
||||||
obs_data_get_int(obj, "action"));
|
obs_data_get_int(obj, "action")));
|
||||||
_macro.Load(obj);
|
_macro.Load(obj);
|
||||||
_actionSelectionType = static_cast<SelectionType>(
|
_actionSelectionType = static_cast<SelectionType>(
|
||||||
obs_data_get_int(obj, "actionSelectionType"));
|
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 void populateActionSelection(QComboBox *list)
|
||||||
{
|
{
|
||||||
static const std::vector<std::pair<MacroActionMacro::Action, std::string>>
|
static const std::vector<std::pair<MacroActionMacro::Action, std::string>>
|
||||||
@@ -327,6 +412,8 @@ static void populateActionSelection(QComboBox *list)
|
|||||||
"AdvSceneSwitcher.action.macro.type.enableAction"},
|
"AdvSceneSwitcher.action.macro.type.enableAction"},
|
||||||
{MacroActionMacro::Action::TOGGLE_ACTION,
|
{MacroActionMacro::Action::TOGGLE_ACTION,
|
||||||
"AdvSceneSwitcher.action.macro.type.toggleAction"},
|
"AdvSceneSwitcher.action.macro.type.toggleAction"},
|
||||||
|
{MacroActionMacro::Action::GET_INFO,
|
||||||
|
"AdvSceneSwitcher.action.macro.type.getInfo"},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const auto &[value, name] : actions) {
|
for (const auto &[value, name] : actions) {
|
||||||
@@ -496,7 +583,7 @@ void MacroActionMacroEdit::UpdateEntryData()
|
|||||||
}
|
}
|
||||||
|
|
||||||
_actions->setCurrentIndex(
|
_actions->setCurrentIndex(
|
||||||
_actions->findData(static_cast<int>(_entryData->_action)));
|
_actions->findData(static_cast<int>(_entryData->GetAction())));
|
||||||
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
|
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
|
||||||
static_cast<int>(_entryData->_actionSelectionType)));
|
static_cast<int>(_entryData->_actionSelectionType)));
|
||||||
_actionIndex->SetValue(_entryData->_actionIndex);
|
_actionIndex->SetValue(_entryData->_actionIndex);
|
||||||
@@ -545,8 +632,8 @@ void MacroActionMacroEdit::MacroChanged(const QString &text)
|
|||||||
void MacroActionMacroEdit::ActionChanged(int idx)
|
void MacroActionMacroEdit::ActionChanged(int idx)
|
||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_action = static_cast<MacroActionMacro::Action>(
|
_entryData->SetAction(static_cast<MacroActionMacro::Action>(
|
||||||
_actions->itemData(idx).toInt());
|
_actions->itemData(idx).toInt()));
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +752,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto action = _entryData->_action;
|
const auto action = _entryData->GetAction();
|
||||||
const char *layoutText = "";
|
const char *layoutText = "";
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MacroActionMacro::Action::PAUSE:
|
case MacroActionMacro::Action::PAUSE:
|
||||||
@@ -674,6 +761,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
|||||||
case MacroActionMacro::Action::RESET_COUNTER:
|
case MacroActionMacro::Action::RESET_COUNTER:
|
||||||
case MacroActionMacro::Action::STOP:
|
case MacroActionMacro::Action::STOP:
|
||||||
case MacroActionMacro::Action::NESTED_MACRO:
|
case MacroActionMacro::Action::NESTED_MACRO:
|
||||||
|
case MacroActionMacro::Action::GET_INFO:
|
||||||
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
|
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
|
||||||
break;
|
break;
|
||||||
case MacroActionMacro::Action::RUN_ACTIONS:
|
case MacroActionMacro::Action::RUN_ACTIONS:
|
||||||
|
|||||||
@@ -56,11 +56,14 @@ public:
|
|||||||
TOGGLE_ACTION,
|
TOGGLE_ACTION,
|
||||||
TOGGLE_PAUSE,
|
TOGGLE_PAUSE,
|
||||||
NESTED_MACRO,
|
NESTED_MACRO,
|
||||||
|
GET_INFO,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void SetAction(Action);
|
||||||
|
Action GetAction() const { return _action; }
|
||||||
|
|
||||||
enum class SelectionType { INDEX, LABEL, ID };
|
enum class SelectionType { INDEX, LABEL, ID };
|
||||||
|
|
||||||
Action _action = Action::NESTED_MACRO;
|
|
||||||
SelectionType _actionSelectionType = SelectionType::INDEX;
|
SelectionType _actionSelectionType = SelectionType::INDEX;
|
||||||
bool _useElseSection = false;
|
bool _useElseSection = false;
|
||||||
IntVariable _actionIndex = 1;
|
IntVariable _actionIndex = 1;
|
||||||
@@ -72,9 +75,12 @@ public:
|
|||||||
int _customWidgetHeight = 0;
|
int _customWidgetHeight = 0;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void SetupTempVars();
|
||||||
void RunActions(Macro *actionMacro) const;
|
void RunActions(Macro *actionMacro) const;
|
||||||
void AdjustActionState(Macro *) const;
|
void AdjustActionState(Macro *) const;
|
||||||
|
|
||||||
|
Action _action = Action::NESTED_MACRO;
|
||||||
|
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
static const std::string id;
|
static const std::string id;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ namespace advss {
|
|||||||
|
|
||||||
const std::string MacroActionVariable::id = "variable";
|
const std::string MacroActionVariable::id = "variable";
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
|
||||||
|
{
|
||||||
|
if (!_tempVar.HasValidID()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {_tempVar};
|
||||||
|
}
|
||||||
|
|
||||||
bool MacroActionVariable::_registered = MacroActionFactory::Register(
|
bool MacroActionVariable::_registered = MacroActionFactory::Register(
|
||||||
MacroActionVariable::id,
|
MacroActionVariable::id,
|
||||||
{MacroActionVariable::Create, MacroActionVariableEdit::Create,
|
{MacroActionVariable::Create, MacroActionVariableEdit::Create,
|
||||||
@@ -1450,6 +1458,7 @@ void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
|
|||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_tempVar = var;
|
_entryData->_tempVar = var;
|
||||||
|
IncrementTempVarInUseGeneration();
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
bool PostLoad();
|
bool PostLoad();
|
||||||
std::string GetShortDesc() const;
|
std::string GetShortDesc() const;
|
||||||
std::string GetId() const { return id; };
|
std::string GetId() const { return id; };
|
||||||
|
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||||
std::shared_ptr<MacroAction> Copy() const;
|
std::shared_ptr<MacroAction> Copy() const;
|
||||||
void SetSegmentIndexValue(int);
|
void SetSegmentIndexValue(int);
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ namespace advss {
|
|||||||
|
|
||||||
const std::string MacroConditionTempVar::id = "temp_var";
|
const std::string MacroConditionTempVar::id = "temp_var";
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> MacroConditionTempVar::GetTempVarRefs() const
|
||||||
|
{
|
||||||
|
if (!_tempVar.HasValidID()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {_tempVar};
|
||||||
|
}
|
||||||
|
|
||||||
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
|
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
|
||||||
MacroConditionTempVar::id,
|
MacroConditionTempVar::id,
|
||||||
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
|
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
|
||||||
@@ -266,6 +274,7 @@ void MacroConditionTempVarEdit::VariableChanged(const TempVariableRef &var)
|
|||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_tempVar = var;
|
_entryData->_tempVar = var;
|
||||||
|
IncrementTempVarInUseGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroConditionTempVarEdit::Variable2Changed(const QString &text)
|
void MacroConditionTempVarEdit::Variable2Changed(const QString &text)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
bool Load(obs_data_t *obj);
|
bool Load(obs_data_t *obj);
|
||||||
std::string GetShortDesc() const;
|
std::string GetShortDesc() const;
|
||||||
std::string GetId() const { return id; };
|
std::string GetId() const { return id; };
|
||||||
|
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||||
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
||||||
{
|
{
|
||||||
return std::make_shared<MacroConditionTempVar>(m);
|
return std::make_shared<MacroConditionTempVar>(m);
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ static void runSegmentHighlightChecksHelper(MacroSegmentList *list)
|
|||||||
// highlight its segments if required
|
// highlight its segments if required
|
||||||
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
|
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
|
||||||
if (macroAction &&
|
if (macroAction &&
|
||||||
macroAction->_action ==
|
macroAction->GetAction() ==
|
||||||
MacroActionMacro::Action::NESTED_MACRO) {
|
MacroActionMacro::Action::NESTED_MACRO) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1086,6 +1086,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
|
|||||||
HighlightAction(idx);
|
HighlightAction(idx);
|
||||||
ui->actionsList->SetHelpMsgVisible(false);
|
ui->actionsList->SetHelpMsgVisible(false);
|
||||||
emit(MacroSegmentOrderChanged());
|
emit(MacroSegmentOrderChanged());
|
||||||
|
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
ui->actionsList->ensureWidgetVisible(
|
||||||
|
ui->actionsList->WidgetAt(currentActionIdx));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroEdit::AddMacroAction(int idx)
|
void MacroEdit::AddMacroAction(int idx)
|
||||||
@@ -1389,6 +1394,11 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
|
|||||||
HighlightElseAction(idx);
|
HighlightElseAction(idx);
|
||||||
ui->elseActionsList->SetHelpMsgVisible(false);
|
ui->elseActionsList->SetHelpMsgVisible(false);
|
||||||
emit(MacroSegmentOrderChanged());
|
emit(MacroSegmentOrderChanged());
|
||||||
|
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
ui->elseActionsList->ensureWidgetVisible(
|
||||||
|
ui->elseActionsList->WidgetAt(currentElseActionIdx));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroEdit::AddMacroElseAction(int idx)
|
void MacroEdit::AddMacroElseAction(int idx)
|
||||||
@@ -1591,6 +1601,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
|
|||||||
HighlightCondition(idx);
|
HighlightCondition(idx);
|
||||||
ui->conditionsList->SetHelpMsgVisible(false);
|
ui->conditionsList->SetHelpMsgVisible(false);
|
||||||
emit(MacroSegmentOrderChanged());
|
emit(MacroSegmentOrderChanged());
|
||||||
|
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
ui->conditionsList->ensureWidgetVisible(
|
||||||
|
ui->conditionsList->WidgetAt(currentConditionIdx));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroEdit::on_conditionAdd_clicked()
|
void MacroEdit::on_conditionAdd_clicked()
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> MacroSegment::GetTempVarRefs() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
|
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
|
||||||
: _macro(m),
|
: _macro(m),
|
||||||
_supportsVariableValue(supportsVariableValue)
|
_supportsVariableValue(supportsVariableValue)
|
||||||
@@ -152,6 +157,16 @@ void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
|
|||||||
NotifyUIAboutTempVarChange(this);
|
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,
|
void MacroSegment::SetTempVarValue(const std::string &id,
|
||||||
const std::string &value)
|
const std::string &value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public:
|
|||||||
std::string GetCustomLabel() const { return _customLabel; }
|
std::string GetCustomLabel() const { return _customLabel; }
|
||||||
virtual bool Save(obs_data_t *obj) const = 0;
|
virtual bool Save(obs_data_t *obj) const = 0;
|
||||||
virtual bool Load(obs_data_t *obj) = 0;
|
virtual bool Load(obs_data_t *obj) = 0;
|
||||||
|
virtual std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||||
virtual bool PostLoad();
|
virtual bool PostLoad();
|
||||||
virtual std::string GetShortDesc() const;
|
virtual std::string GetShortDesc() const;
|
||||||
virtual std::string GetId() const = 0;
|
virtual std::string GetId() const = 0;
|
||||||
@@ -54,6 +55,7 @@ protected:
|
|||||||
void AddTempvar(const std::string &id, const std::string &name,
|
void AddTempvar(const std::string &id, const std::string &name,
|
||||||
const std::string &description = "");
|
const std::string &description = "");
|
||||||
|
|
||||||
|
bool IsTempVarInUse(const std::string &id) const;
|
||||||
void SetTempVarValue(const std::string &id, const std::string &value);
|
void SetTempVarValue(const std::string &id, const std::string &value);
|
||||||
|
|
||||||
template<typename T, typename = std::enable_if_t<
|
template<typename T, typename = std::enable_if_t<
|
||||||
@@ -64,6 +66,14 @@ protected:
|
|||||||
: std::string("false"));
|
: 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:
|
private:
|
||||||
void ClearAvailableTempvars();
|
void ClearAvailableTempvars();
|
||||||
std::optional<const TempVariable>
|
std::optional<const TempVariable>
|
||||||
|
|||||||
@@ -670,18 +670,6 @@ void AdvSceneSwitcher::SetupMacroTab()
|
|||||||
SLOT(HighlightOnChange()));
|
SLOT(HighlightOnChange()));
|
||||||
onChangeHighlightTimer.start();
|
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,
|
SetupMacroSearchWidgets(ui->macroSearchLayout, ui->macroSearchText,
|
||||||
ui->macroSearchClear, ui->macroSearchType,
|
ui->macroSearchClear, ui->macroSearchType,
|
||||||
ui->macroSearchRegex,
|
ui->macroSearchRegex,
|
||||||
@@ -705,6 +693,26 @@ void AdvSceneSwitcher::SetupMacroTab()
|
|||||||
ui->actionTriggerMode->addItem(obs_module_text(name),
|
ui->actionTriggerMode->addItem(obs_module_text(name),
|
||||||
static_cast<int>(mode));
|
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)
|
void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
#include "log-helper.hpp"
|
#include "log-helper.hpp"
|
||||||
#include "obs-module-helper.hpp"
|
#include "obs-module-helper.hpp"
|
||||||
#include "plugin-state-helpers.hpp"
|
#include "plugin-state-helpers.hpp"
|
||||||
#include "ui-helpers.hpp"
|
|
||||||
|
|
||||||
#include <obs-frontend-api.h>
|
#include <obs-frontend-api.h>
|
||||||
#include <obs-module.h>
|
#include <obs-module.h>
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QLabel>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QTimer>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -25,10 +26,29 @@ static constexpr bool handleUncleanShutdown = true;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static bool wasCleanShutdown = false;
|
static bool wasCleanShutdown = false;
|
||||||
|
static bool suppressCrashDialog = false;
|
||||||
|
|
||||||
|
bool GetSuppressCrashDialog()
|
||||||
|
{
|
||||||
|
return suppressCrashDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSuppressCrashDialog(bool suppress)
|
||||||
|
{
|
||||||
|
suppressCrashDialog = suppress;
|
||||||
|
}
|
||||||
|
|
||||||
static void setup();
|
static void setup();
|
||||||
static bool setupDone = []() {
|
static bool setupDone = []() {
|
||||||
AddPluginInitStep(setup);
|
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;
|
return true;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
@@ -108,8 +128,39 @@ static bool wasUncleanShutdown()
|
|||||||
|
|
||||||
static void askForStartupSkip()
|
static void askForStartupSkip()
|
||||||
{
|
{
|
||||||
bool skipStart = DisplayMessage(
|
auto mainWindow =
|
||||||
obs_module_text("AdvSceneSwitcher.crashDetected"), true, false);
|
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) {
|
if (!skipStart) {
|
||||||
StartPlugin();
|
StartPlugin();
|
||||||
}
|
}
|
||||||
@@ -121,6 +172,10 @@ bool ShouldSkipPluginStartOnUncleanShutdown()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (suppressCrashDialog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// This function is called while the plugin settings are being loaded.
|
// This function is called while the plugin settings are being loaded.
|
||||||
// Blocking at this stage can cause issues such as OBS failing to start
|
// Blocking at this stage can cause issues such as OBS failing to start
|
||||||
// or crashing.
|
// or crashing.
|
||||||
|
|||||||
@@ -4,4 +4,7 @@ namespace advss {
|
|||||||
|
|
||||||
bool ShouldSkipPluginStartOnUncleanShutdown();
|
bool ShouldSkipPluginStartOnUncleanShutdown();
|
||||||
|
|
||||||
|
bool GetSuppressCrashDialog();
|
||||||
|
void SetSuppressCrashDialog(bool suppress);
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent)
|
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent,
|
||||||
|
const QString &browseTitle)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
_type(type),
|
_type(type),
|
||||||
|
_browseTitle(browseTitle),
|
||||||
_filePath(new VariableLineEdit(this)),
|
_filePath(new VariableLineEdit(this)),
|
||||||
_browseButton(
|
_browseButton(
|
||||||
new QPushButton(obs_module_text("AdvSceneSwitcher.browse")))
|
new QPushButton(obs_module_text("AdvSceneSwitcher.browse")))
|
||||||
@@ -55,11 +57,14 @@ void FileSelection::BrowseButtonClicked()
|
|||||||
QString defaultPath = ValidPathOrDesktop(_filePath->text());
|
QString defaultPath = ValidPathOrDesktop(_filePath->text());
|
||||||
QString path;
|
QString path;
|
||||||
if (_type == FileSelection::Type::WRITE) {
|
if (_type == FileSelection::Type::WRITE) {
|
||||||
path = QFileDialog::getSaveFileName(this, "", defaultPath);
|
path = QFileDialog::getSaveFileName(this, _browseTitle,
|
||||||
|
defaultPath);
|
||||||
} else if (_type == FileSelection::Type::READ) {
|
} else if (_type == FileSelection::Type::READ) {
|
||||||
path = QFileDialog::getOpenFileName(this, "", defaultPath);
|
path = QFileDialog::getOpenFileName(this, _browseTitle,
|
||||||
|
defaultPath);
|
||||||
} else {
|
} else {
|
||||||
path = QFileDialog::getExistingDirectory(this, "", defaultPath);
|
path = QFileDialog::getExistingDirectory(this, _browseTitle,
|
||||||
|
defaultPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public:
|
|||||||
|
|
||||||
EXPORT
|
EXPORT
|
||||||
FileSelection(FileSelection::Type type = FileSelection::Type::READ,
|
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 StringVariable &);
|
||||||
EXPORT void SetPath(const QString &);
|
EXPORT void SetPath(const QString &);
|
||||||
EXPORT QString GetPath() const;
|
EXPORT QString GetPath() const;
|
||||||
@@ -34,6 +34,7 @@ signals:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Type _type;
|
Type _type;
|
||||||
|
QString _browseTitle;
|
||||||
VariableLineEdit *_filePath;
|
VariableLineEdit *_filePath;
|
||||||
QPushButton *_browseButton;
|
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,
|
void ListControls::AddActionHelper(const char *theme, const char *className,
|
||||||
const char *tooltip,
|
const char *tooltip,
|
||||||
const std::function<void()> &signal)
|
const std::function<void()> &signal)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class ADVSS_EXPORT ListControls final : public QToolBar {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
ListControls(QWidget *parent = nullptr, bool reorder = true);
|
ListControls(QWidget *parent = nullptr, bool reorder = true);
|
||||||
|
void AddWidget(QWidget *widget);
|
||||||
|
void AddSeparator();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void Add();
|
void Add();
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#include "list-editor.hpp"
|
#include "list-editor.hpp"
|
||||||
#include "ui-helpers.hpp"
|
#include "ui-helpers.hpp"
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
ListEditor::ListEditor(QWidget *parent, bool reorder)
|
ListEditor::ListEditor(QWidget *parent, bool reorder)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
_list(new QListWidget()),
|
_list(new QListWidget()),
|
||||||
_controls(new ListControls(this, reorder)),
|
_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(Add()), this, SLOT(Add()));
|
||||||
QWidget::connect(_controls, SIGNAL(Remove()), this, SLOT(Remove()));
|
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(_controls, SIGNAL(Down()), this, SLOT(Down()));
|
||||||
QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)),
|
QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)),
|
||||||
this, SLOT(Clicked(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->setContentsMargins(0, 0, 0, 0);
|
||||||
_mainLayout->addWidget(_list);
|
_mainLayout->addWidget(_list);
|
||||||
@@ -22,6 +38,40 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
|
|||||||
setLayout(_mainLayout);
|
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)
|
void ListEditor::showEvent(QShowEvent *e)
|
||||||
{
|
{
|
||||||
QWidget::showEvent(e);
|
QWidget::showEvent(e);
|
||||||
@@ -64,6 +114,24 @@ int ListEditor::GetIndexOfSignal() const
|
|||||||
void ListEditor::UpdateListSize()
|
void ListEditor::UpdateListSize()
|
||||||
{
|
{
|
||||||
SetHeightToContentHeight(_list);
|
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();
|
adjustSize();
|
||||||
updateGeometry();
|
updateGeometry();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "export-symbol-helper.hpp"
|
#include "export-symbol-helper.hpp"
|
||||||
#include "list-controls.hpp"
|
#include "list-controls.hpp"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
|
|
||||||
@@ -13,9 +14,13 @@ class ADVSS_EXPORT ListEditor : public QWidget {
|
|||||||
public:
|
public:
|
||||||
ListEditor(QWidget *parent = nullptr, bool reorder = true);
|
ListEditor(QWidget *parent = nullptr, bool reorder = true);
|
||||||
int count() const { return _list->count(); };
|
int count() const { return _list->count(); };
|
||||||
|
void SetPlaceholderText(const QString &text);
|
||||||
|
void SetMinListHeight(int);
|
||||||
|
void SetMaxListHeight(int);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void showEvent(QShowEvent *);
|
void showEvent(QShowEvent *);
|
||||||
|
bool eventFilter(QObject *, QEvent *);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
virtual void Add() = 0;
|
virtual void Add() = 0;
|
||||||
@@ -23,6 +28,7 @@ private slots:
|
|||||||
virtual void Up(){};
|
virtual void Up(){};
|
||||||
virtual void Down(){};
|
virtual void Down(){};
|
||||||
virtual void Clicked(QListWidgetItem *) {}
|
virtual void Clicked(QListWidgetItem *) {}
|
||||||
|
void UpdatePlaceholder();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void UpdateListSize();
|
void UpdateListSize();
|
||||||
@@ -31,6 +37,11 @@ protected:
|
|||||||
QListWidget *_list;
|
QListWidget *_list;
|
||||||
ListControls *_controls;
|
ListControls *_controls;
|
||||||
QVBoxLayout *_mainLayout;
|
QVBoxLayout *_mainLayout;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel *_placeholder;
|
||||||
|
int _minHeight = -1;
|
||||||
|
int _maxHeight = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -9,19 +9,15 @@ namespace advss {
|
|||||||
|
|
||||||
std::variant<double, std::string> EvalMathExpression(const std::string &expr)
|
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 = []() {
|
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);
|
return dis(gen);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!setupDone) {
|
exprtk::symbol_table<double> symbolTable;
|
||||||
symbolTable.add_function("random", randomFunc);
|
symbolTable.add_function("random", randomFunc);
|
||||||
setupDone = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
exprtk::expression<double> expression;
|
exprtk::expression<double> expression;
|
||||||
expression.register_symbol_table(symbolTable);
|
expression.register_symbol_table(symbolTable);
|
||||||
|
|||||||
@@ -66,4 +66,15 @@ void Lockable::WithLock(const std::function<void()> &func)
|
|||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SuspendLock::SuspendLock(Lockable &lockable)
|
||||||
|
: _mtx(static_cast<std::mutex &>(lockable._mtx))
|
||||||
|
{
|
||||||
|
_mtx.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
SuspendLock::~SuspendLock()
|
||||||
|
{
|
||||||
|
_mtx.lock();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -51,6 +51,24 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
PerInstanceMutex _mtx;
|
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
|
} // namespace advss
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(advss::TempVariableRef);
|
Q_DECLARE_METATYPE(advss::TempVariableRef);
|
||||||
|
|
||||||
|
static std::atomic<uint64_t> tempVarInUseGeneration{0};
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
TempVariable::TempVariable(const std::string &id, const std::string &name,
|
TempVariable::TempVariable(const std::string &id, const std::string &name,
|
||||||
@@ -120,6 +123,60 @@ TempVariableRef TempVariable::GetRef() const
|
|||||||
return ref;
|
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
|
TempVariableRef::SegmentType TempVariableRef::GetType() const
|
||||||
{
|
{
|
||||||
auto segment = _segment.lock();
|
auto segment = _segment.lock();
|
||||||
@@ -665,13 +722,13 @@ TempVarSignalManager *TempVarSignalManager::Instance()
|
|||||||
|
|
||||||
void NotifyUIAboutTempVarChange(MacroSegment *segment)
|
void NotifyUIAboutTempVarChange(MacroSegment *segment)
|
||||||
{
|
{
|
||||||
obs_queue_task(
|
IncrementTempVarInUseGeneration();
|
||||||
OBS_TASK_UI,
|
QueueUITask(
|
||||||
[](void *segment) {
|
[](void *segment) {
|
||||||
TempVarSignalManager::Instance()->SegmentTempVarsChanged(
|
TempVarSignalManager::Instance()->SegmentTempVarsChanged(
|
||||||
(MacroSegment *)segment);
|
(MacroSegment *)segment);
|
||||||
},
|
},
|
||||||
segment, false);
|
segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public:
|
|||||||
void SetValue(const std::string &val);
|
void SetValue(const std::string &val);
|
||||||
void InvalidateValue();
|
void InvalidateValue();
|
||||||
TempVariableRef GetRef() const;
|
TempVariableRef GetRef() const;
|
||||||
|
EXPORT bool IsInUse() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string _id = "";
|
std::string _id = "";
|
||||||
@@ -53,6 +54,8 @@ private:
|
|||||||
mutable std::mutex _lastValuesMutex;
|
mutable std::mutex _lastValuesMutex;
|
||||||
std::vector<std::string> _lastValues;
|
std::vector<std::string> _lastValues;
|
||||||
bool _valueIsValid = false;
|
bool _valueIsValid = false;
|
||||||
|
mutable bool _isInUseCache = false;
|
||||||
|
mutable uint64_t _isInUseCacheGeneration = UINT64_MAX;
|
||||||
|
|
||||||
std::weak_ptr<MacroSegment> _segment;
|
std::weak_ptr<MacroSegment> _segment;
|
||||||
friend TempVariableSelection;
|
friend TempVariableSelection;
|
||||||
@@ -122,5 +125,6 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
void NotifyUIAboutTempVarChange(MacroSegment *);
|
void NotifyUIAboutTempVarChange(MacroSegment *);
|
||||||
|
EXPORT void IncrementTempVarInUseGeneration();
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ void StringVariable::Resolve() const
|
|||||||
_resolvedValue = _value;
|
_resolvedValue = _value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_lastResolve == GetLastVariableChangeTime()) {
|
const auto lastChange = GetLastVariableChangeTime();
|
||||||
|
if (_lastResolve == lastChange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_resolvedValue = SubstitueVariables(_value);
|
_resolvedValue = SubstitueVariables(_value);
|
||||||
_lastResolve = GetLastVariableChangeTime();
|
_lastResolve = lastChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringVariable::operator std::string() const
|
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 auto &variable = std::dynamic_pointer_cast<Variable>(v);
|
||||||
const std::string pattern = "${" + variable->Name() + "}";
|
const std::string pattern = "${" + variable->Name() + "}";
|
||||||
if (ReplaceAll(str, pattern, variable->Value(false))) {
|
if (ReplaceAll(str, pattern, variable->Value(false))) {
|
||||||
variable->UpdateLastUsed();
|
variable->MarkAsUsed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str;
|
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
|
// Keep track of the last time a variable was changed to save some work when
|
||||||
// when resolving strings containing variables, etc.
|
// when resolving strings containing variables, etc.
|
||||||
|
static std::mutex lastVariableChangeMutex;
|
||||||
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
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()
|
Variable::Variable() : Item()
|
||||||
{
|
{
|
||||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
setLastVariableChangeTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
Variable::~Variable()
|
Variable::~Variable()
|
||||||
{
|
{
|
||||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
setLastVariableChangeTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Variable::Load(obs_data_t *obj)
|
void Variable::Load(obs_data_t *obj)
|
||||||
@@ -37,7 +44,7 @@ void Variable::Load(obs_data_t *obj)
|
|||||||
SetValue(_defaultValue);
|
SetValue(_defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
setLastVariableChangeTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Variable::Save(obs_data_t *obj) const
|
void Variable::Save(obs_data_t *obj) const
|
||||||
@@ -45,8 +52,11 @@ void Variable::Save(obs_data_t *obj) const
|
|||||||
Item::Save(obj);
|
Item::Save(obj);
|
||||||
obs_data_set_int(obj, "saveAction", static_cast<int>(_saveAction));
|
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());
|
obs_data_set_string(obj, "defaultValue", _defaultValue.c_str());
|
||||||
@@ -62,6 +72,18 @@ std::string Variable::Value(bool updateLastUsed) const
|
|||||||
return _value;
|
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
|
std::optional<double> Variable::DoubleValue() const
|
||||||
{
|
{
|
||||||
return GetDouble(Value());
|
return GetDouble(Value());
|
||||||
@@ -79,8 +101,11 @@ void Variable::SetValue(const std::string &value)
|
|||||||
_value = value;
|
_value = value;
|
||||||
|
|
||||||
UpdateLastUsed();
|
UpdateLastUsed();
|
||||||
UpdateLastChanged();
|
if (_previousValue != _value) {
|
||||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
_lastChanged = std::chrono::high_resolution_clock::now();
|
||||||
|
++_valueChangeCount;
|
||||||
|
}
|
||||||
|
setLastVariableChangeTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Variable::SetValue(double value)
|
void Variable::SetValue(double value)
|
||||||
@@ -90,6 +115,7 @@ void Variable::SetValue(double value)
|
|||||||
|
|
||||||
std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
|
std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
if (_lastUsed.time_since_epoch().count() == 0) {
|
if (_lastUsed.time_since_epoch().count() == 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -101,6 +127,7 @@ std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
|
|||||||
|
|
||||||
std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const
|
std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
if (_lastChanged.time_since_epoch().count() == 0) {
|
if (_lastChanged.time_since_epoch().count() == 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -116,12 +143,10 @@ void Variable::UpdateLastUsed() const
|
|||||||
_lastUsed = std::chrono::high_resolution_clock::now();
|
_lastUsed = std::chrono::high_resolution_clock::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Variable::UpdateLastChanged()
|
void Variable::MarkAsUsed() const
|
||||||
{
|
{
|
||||||
if (_previousValue != _value) {
|
std::lock_guard<std::mutex> lock(_mutex);
|
||||||
_lastChanged = std::chrono::high_resolution_clock::now();
|
UpdateLastUsed();
|
||||||
++_valueChangeCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void populateSaveActionSelection(QComboBox *list)
|
static void populateSaveActionSelection(QComboBox *list)
|
||||||
@@ -206,7 +231,7 @@ bool VariableSettingsDialog::AskForSettings(QWidget *parent, Variable &settings)
|
|||||||
dialog._defaultValue->toPlainText().toStdString();
|
dialog._defaultValue->toPlainText().toStdString();
|
||||||
settings._saveAction =
|
settings._saveAction =
|
||||||
static_cast<Variable::SaveAction>(dialog._save->currentIndex());
|
static_cast<Variable::SaveAction>(dialog._save->currentIndex());
|
||||||
lastVariableChange = std::chrono::high_resolution_clock::now();
|
setLastVariableChangeTime();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -461,6 +486,7 @@ void ImportVariables(obs_data_t *data)
|
|||||||
|
|
||||||
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()
|
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
|
||||||
return lastVariableChange;
|
return lastVariableChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,16 +34,15 @@ public:
|
|||||||
EXPORT std::string Value(bool updateLastUsed = true) const;
|
EXPORT std::string Value(bool updateLastUsed = true) const;
|
||||||
EXPORT std::optional<double> DoubleValue() const;
|
EXPORT std::optional<double> DoubleValue() const;
|
||||||
EXPORT std::optional<int> IntValue() const;
|
EXPORT std::optional<int> IntValue() const;
|
||||||
std::string GetPreviousValue() const { return _previousValue; };
|
std::string GetPreviousValue() const;
|
||||||
std::string GetDefaultValue() const { return _defaultValue; }
|
std::string GetDefaultValue() const { return _defaultValue; }
|
||||||
EXPORT void SetValue(const std::string &value);
|
EXPORT void SetValue(const std::string &value);
|
||||||
void SetValue(double value);
|
void SetValue(double value);
|
||||||
SaveAction GetSaveAction() const { return _saveAction; }
|
SaveAction GetSaveAction() const { return _saveAction; }
|
||||||
int GetValueChangeCount() const { return _valueChangeCount; }
|
int GetValueChangeCount() const;
|
||||||
std::optional<uint64_t> GetSecondsSinceLastUse() const;
|
std::optional<uint64_t> GetSecondsSinceLastUse() const;
|
||||||
std::optional<uint64_t> GetSecondsSinceLastChange() const;
|
std::optional<uint64_t> GetSecondsSinceLastChange() const;
|
||||||
void UpdateLastUsed() const;
|
void MarkAsUsed() const;
|
||||||
void UpdateLastChanged();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SaveAction _saveAction = SaveAction::DONT_SAVE;
|
SaveAction _saveAction = SaveAction::DONT_SAVE;
|
||||||
@@ -55,6 +54,8 @@ private:
|
|||||||
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
|
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
|
||||||
mutable std::mutex _mutex;
|
mutable std::mutex _mutex;
|
||||||
|
|
||||||
|
void UpdateLastUsed() const;
|
||||||
|
|
||||||
friend VariableSelection;
|
friend VariableSelection;
|
||||||
friend VariableSettingsDialog;
|
friend VariableSettingsDialog;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ target_sources(
|
|||||||
macro-action-obs-settings.hpp
|
macro-action-obs-settings.hpp
|
||||||
macro-action-osc.cpp
|
macro-action-osc.cpp
|
||||||
macro-action-osc.hpp
|
macro-action-osc.hpp
|
||||||
|
macro-action-play-audio.cpp
|
||||||
|
macro-action-play-audio.hpp
|
||||||
macro-action-plugin-state.cpp
|
macro-action-plugin-state.cpp
|
||||||
macro-action-plugin-state.hpp
|
macro-action-plugin-state.hpp
|
||||||
macro-action-profile.cpp
|
macro-action-profile.cpp
|
||||||
@@ -57,6 +59,8 @@ target_sources(
|
|||||||
macro-action-sequence.hpp
|
macro-action-sequence.hpp
|
||||||
macro-action-source.cpp
|
macro-action-source.cpp
|
||||||
macro-action-source.hpp
|
macro-action-source.hpp
|
||||||
|
macro-action-source-interaction.cpp
|
||||||
|
macro-action-source-interaction.hpp
|
||||||
macro-action-streaming.cpp
|
macro-action-streaming.cpp
|
||||||
macro-action-streaming.hpp
|
macro-action-streaming.hpp
|
||||||
macro-action-studio-mode.cpp
|
macro-action-studio-mode.cpp
|
||||||
@@ -144,7 +148,17 @@ target_sources(
|
|||||||
|
|
||||||
target_sources(
|
target_sources(
|
||||||
${PROJECT_NAME}
|
${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/audio-helpers.hpp
|
||||||
utils/connection-manager.cpp
|
utils/connection-manager.cpp
|
||||||
utils/connection-manager.hpp
|
utils/connection-manager.hpp
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ namespace advss {
|
|||||||
|
|
||||||
const std::string MacroActionFilter::id = "filter";
|
const std::string MacroActionFilter::id = "filter";
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> MacroActionFilter::GetTempVarRefs() const
|
||||||
|
{
|
||||||
|
if (!_tempVar.HasValidID()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {_tempVar};
|
||||||
|
}
|
||||||
|
|
||||||
bool MacroActionFilter::_registered = MacroActionFactory::Register(
|
bool MacroActionFilter::_registered = MacroActionFactory::Register(
|
||||||
MacroActionFilter::id,
|
MacroActionFilter::id,
|
||||||
{MacroActionFilter::Create, MacroActionFilterEdit::Create,
|
{MacroActionFilter::Create, MacroActionFilterEdit::Create,
|
||||||
@@ -428,6 +436,7 @@ void MacroActionFilterEdit::SelectionChanged(const TempVariableRef &var)
|
|||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_tempVar = var;
|
_entryData->_tempVar = var;
|
||||||
|
IncrementTempVarInUseGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)
|
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ public:
|
|||||||
SettingsInputMethod _settingsInputMethod =
|
SettingsInputMethod _settingsInputMethod =
|
||||||
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||||
|
|
||||||
SourceSelection _source;
|
SourceSelection _source;
|
||||||
FilterSelection _filter;
|
FilterSelection _filter;
|
||||||
Action _action = Action::ENABLE;
|
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);
|
obs_media_state state = obs_source_media_get_state(source);
|
||||||
|
|
||||||
@@ -130,6 +130,7 @@ void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
|
|||||||
SeekToPercentage(source);
|
SeekToPercentage(source);
|
||||||
break;
|
break;
|
||||||
case Action::WAIT_FOR_PLAYBACK_STOP: {
|
case Action::WAIT_FOR_PLAYBACK_STOP: {
|
||||||
|
SuspendLock suspendLock(*this);
|
||||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||||
waitHelper(&lock, GetMacro(), source);
|
waitHelper(&lock, GetMacro(), source);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public:
|
|||||||
SceneSelection _scene;
|
SceneSelection _scene;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void PerformActionHelper(obs_source_t *) const;
|
void PerformActionHelper(obs_source_t *);
|
||||||
void SeekToPercentage(obs_source_t *source) const;
|
void SeekToPercentage(obs_source_t *source) const;
|
||||||
|
|
||||||
static bool _registered;
|
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()
|
bool MacroActionRandom::PerformAction()
|
||||||
{
|
{
|
||||||
if (_macros.size() == 0) {
|
if (_macros.size() == 0) {
|
||||||
|
SetTempVarValue("macro", "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat);
|
auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat);
|
||||||
if (macros.size() == 0) {
|
if (macros.size() == 0) {
|
||||||
|
SetTempVarValue("macro", "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (macros.size() == 1) {
|
if (macros.size() == 1) {
|
||||||
lastRandomMacro = macros[0];
|
lastRandomMacro = macros[0];
|
||||||
|
SetTempVarValue("macro", GetMacroName(macros[0].get()));
|
||||||
return RunMacroActions(macros[0].get());
|
return RunMacroActions(macros[0].get());
|
||||||
}
|
}
|
||||||
|
|
||||||
srand((unsigned int)time(0));
|
srand((unsigned int)time(0));
|
||||||
size_t idx = std::rand() % (macros.size());
|
size_t idx = std::rand() % (macros.size());
|
||||||
lastRandomMacro = macros[idx];
|
lastRandomMacro = macros[idx];
|
||||||
|
|
||||||
|
SetTempVarValue("macro", GetMacroName(macros[idx].get()));
|
||||||
|
|
||||||
return RunMacroActions(macros[idx].get());
|
return RunMacroActions(macros[idx].get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +93,11 @@ bool MacroActionRandom::Load(obs_data_t *obj)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MacroActionRandom::PostLoad()
|
||||||
|
{
|
||||||
|
return MacroAction::PostLoad() && MultiMacroRefAction::PostLoad();
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m)
|
std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m)
|
||||||
{
|
{
|
||||||
return std::make_shared<MacroActionRandom>(m);
|
return std::make_shared<MacroActionRandom>(m);
|
||||||
@@ -95,6 +108,16 @@ std::shared_ptr<MacroAction> MacroActionRandom::Copy() const
|
|||||||
return std::make_shared<MacroActionRandom>(*this);
|
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(
|
MacroActionRandomEdit::MacroActionRandomEdit(
|
||||||
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
|
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public:
|
|||||||
void LogAction() const;
|
void LogAction() const;
|
||||||
bool Save(obs_data_t *obj) const;
|
bool Save(obs_data_t *obj) const;
|
||||||
bool Load(obs_data_t *obj);
|
bool Load(obs_data_t *obj);
|
||||||
|
bool PostLoad();
|
||||||
std::string GetId() const { return id; };
|
std::string GetId() const { return id; };
|
||||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||||
std::shared_ptr<MacroAction> Copy() const;
|
std::shared_ptr<MacroAction> Copy() const;
|
||||||
@@ -22,7 +23,10 @@ public:
|
|||||||
bool _allowRepeat = false;
|
bool _allowRepeat = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void SetupTempVars();
|
||||||
|
|
||||||
MacroRef lastRandomMacro;
|
MacroRef lastRandomMacro;
|
||||||
|
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
static const std::string id;
|
static const std::string id;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,12 +22,18 @@ const static std::map<MacroActionRecord::Action, std::string> actionTypes = {
|
|||||||
"AdvSceneSwitcher.action.recording.type.pause"},
|
"AdvSceneSwitcher.action.recording.type.pause"},
|
||||||
{MacroActionRecord::Action::UNPAUSE,
|
{MacroActionRecord::Action::UNPAUSE,
|
||||||
"AdvSceneSwitcher.action.recording.type.unpause"},
|
"AdvSceneSwitcher.action.recording.type.unpause"},
|
||||||
|
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(28, 0, 0)
|
||||||
{MacroActionRecord::Action::SPLIT,
|
{MacroActionRecord::Action::SPLIT,
|
||||||
"AdvSceneSwitcher.action.recording.type.split"},
|
"AdvSceneSwitcher.action.recording.type.split"},
|
||||||
|
#endif
|
||||||
{MacroActionRecord::Action::FOLDER,
|
{MacroActionRecord::Action::FOLDER,
|
||||||
"AdvSceneSwitcher.action.recording.type.changeOutputFolder"},
|
"AdvSceneSwitcher.action.recording.type.changeOutputFolder"},
|
||||||
{MacroActionRecord::Action::FILE_FORMAT,
|
{MacroActionRecord::Action::FILE_FORMAT,
|
||||||
"AdvSceneSwitcher.action.recording.type.changeOutputFileFormat"},
|
"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()
|
bool MacroActionRecord::PerformAction()
|
||||||
@@ -87,6 +93,13 @@ bool MacroActionRecord::PerformAction()
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -110,6 +123,7 @@ bool MacroActionRecord::Save(obs_data_t *obj) const
|
|||||||
obs_data_set_int(obj, "action", static_cast<int>(_action));
|
obs_data_set_int(obj, "action", static_cast<int>(_action));
|
||||||
_folder.Save(obj, "folder");
|
_folder.Save(obj, "folder");
|
||||||
_fileFormat.Save(obj, "format");
|
_fileFormat.Save(obj, "format");
|
||||||
|
_chapterName.Save(obj, "chapterName");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +133,7 @@ bool MacroActionRecord::Load(obs_data_t *obj)
|
|||||||
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
|
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
|
||||||
_folder.Load(obj, "folder");
|
_folder.Load(obj, "folder");
|
||||||
_fileFormat.Load(obj, "format");
|
_fileFormat.Load(obj, "format");
|
||||||
|
_chapterName.Load(obj, "chapterName");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +151,7 @@ void MacroActionRecord::ResolveVariablesToFixedValues()
|
|||||||
{
|
{
|
||||||
_folder.ResolveVariables();
|
_folder.ResolveVariables();
|
||||||
_fileFormat.ResolveVariables();
|
_fileFormat.ResolveVariables();
|
||||||
|
_chapterName.ResolveVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void populateActionSelection(QComboBox *list)
|
static inline void populateActionSelection(QComboBox *list)
|
||||||
@@ -154,7 +170,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
|||||||
_splitHint(new QLabel(obs_module_text(
|
_splitHint(new QLabel(obs_module_text(
|
||||||
"AdvSceneSwitcher.action.recording.split.hint"))),
|
"AdvSceneSwitcher.action.recording.split.hint"))),
|
||||||
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
|
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
|
||||||
_recordFileFormat(new VariableLineEdit(this))
|
_recordFileFormat(new VariableLineEdit(this)),
|
||||||
|
_chapterName(new VariableLineEdit(this))
|
||||||
{
|
{
|
||||||
populateActionSelection(_actions);
|
populateActionSelection(_actions);
|
||||||
|
|
||||||
@@ -164,6 +181,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
|||||||
this, SLOT(FolderChanged(const QString &)));
|
this, SLOT(FolderChanged(const QString &)));
|
||||||
QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this,
|
QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this,
|
||||||
SLOT(FormatStringChanged()));
|
SLOT(FormatStringChanged()));
|
||||||
|
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
|
||||||
|
SLOT(ChapterNameChanged()));
|
||||||
|
|
||||||
auto mainLayout = new QHBoxLayout;
|
auto mainLayout = new QHBoxLayout;
|
||||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
|
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
|
||||||
@@ -172,7 +191,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
|||||||
{"{{pauseHint}}", _pauseHint},
|
{"{{pauseHint}}", _pauseHint},
|
||||||
{"{{splitHint}}", _splitHint},
|
{"{{splitHint}}", _splitHint},
|
||||||
{"{{recordFolder}}", _recordFolder},
|
{"{{recordFolder}}", _recordFolder},
|
||||||
{"{{recordFileFormat}}", _recordFileFormat}});
|
{"{{recordFileFormat}}", _recordFileFormat},
|
||||||
|
{"{{chapterName}}", _chapterName}});
|
||||||
setLayout(mainLayout);
|
setLayout(mainLayout);
|
||||||
|
|
||||||
_entryData = entryData;
|
_entryData = entryData;
|
||||||
@@ -188,6 +208,7 @@ void MacroActionRecordEdit::UpdateEntryData()
|
|||||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
||||||
_recordFolder->SetPath(_entryData->_folder);
|
_recordFolder->SetPath(_entryData->_folder);
|
||||||
_recordFileFormat->setText(_entryData->_fileFormat);
|
_recordFileFormat->setText(_entryData->_fileFormat);
|
||||||
|
_chapterName->setText(_entryData->_chapterName);
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +230,12 @@ void MacroActionRecordEdit::FormatStringChanged()
|
|||||||
_entryData->_fileFormat = _recordFileFormat->text().toStdString();
|
_entryData->_fileFormat = _recordFileFormat->text().toStdString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MacroActionRecordEdit::ChapterNameChanged()
|
||||||
|
{
|
||||||
|
GUARD_LOADING_AND_LOCK();
|
||||||
|
_entryData->_chapterName = _chapterName->text().toStdString();
|
||||||
|
}
|
||||||
|
|
||||||
void MacroActionRecordEdit::SetWidgetVisibility()
|
void MacroActionRecordEdit::SetWidgetVisibility()
|
||||||
{
|
{
|
||||||
_pauseHint->setVisible(isPauseAction(_entryData->_action));
|
_pauseHint->setVisible(isPauseAction(_entryData->_action));
|
||||||
@@ -218,6 +245,8 @@ void MacroActionRecordEdit::SetWidgetVisibility()
|
|||||||
MacroActionRecord::Action::FOLDER);
|
MacroActionRecord::Action::FOLDER);
|
||||||
_recordFileFormat->setVisible(_entryData->_action ==
|
_recordFileFormat->setVisible(_entryData->_action ==
|
||||||
MacroActionRecord::Action::FILE_FORMAT);
|
MacroActionRecord::Action::FILE_FORMAT);
|
||||||
|
_chapterName->setVisible(_entryData->_action ==
|
||||||
|
MacroActionRecord::Action::ADD_CHAPTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroActionRecordEdit::ActionChanged(int value)
|
void MacroActionRecordEdit::ActionChanged(int value)
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ public:
|
|||||||
SPLIT,
|
SPLIT,
|
||||||
FOLDER,
|
FOLDER,
|
||||||
FILE_FORMAT,
|
FILE_FORMAT,
|
||||||
|
ADD_CHAPTER,
|
||||||
};
|
};
|
||||||
Action _action = Action::STOP;
|
Action _action = Action::STOP;
|
||||||
|
|
||||||
StringVariable _folder = QDir::homePath().toStdString() + "/Videos";
|
StringVariable _folder = QDir::homePath().toStdString() + "/Videos";
|
||||||
StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss";
|
StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss";
|
||||||
|
StringVariable _chapterName = "";
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
@@ -60,6 +62,7 @@ private slots:
|
|||||||
void ActionChanged(int value);
|
void ActionChanged(int value);
|
||||||
void FolderChanged(const QString &);
|
void FolderChanged(const QString &);
|
||||||
void FormatStringChanged();
|
void FormatStringChanged();
|
||||||
|
void ChapterNameChanged();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QComboBox *_actions;
|
QComboBox *_actions;
|
||||||
@@ -67,6 +70,7 @@ protected:
|
|||||||
QLabel *_splitHint;
|
QLabel *_splitHint;
|
||||||
FileSelection *_recordFolder;
|
FileSelection *_recordFolder;
|
||||||
VariableLineEdit *_recordFileFormat;
|
VariableLineEdit *_recordFileFormat;
|
||||||
|
VariableLineEdit *_chapterName;
|
||||||
std::shared_ptr<MacroActionRecord> _entryData;
|
std::shared_ptr<MacroActionRecord> _entryData;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -15,9 +15,20 @@ bool MacroActionRun::_registered = MacroActionFactory::Register(
|
|||||||
bool MacroActionRun::PerformAction()
|
bool MacroActionRun::PerformAction()
|
||||||
{
|
{
|
||||||
if (_wait) {
|
if (_wait) {
|
||||||
_procConfig.StartProcessAndWait(_timeout.Milliseconds());
|
// Snapshot config before releasing the lock for the blocking wait
|
||||||
SetTempVarValues();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,13 +82,22 @@ void MacroActionSequence::ResolveVariablesToFixedValues()
|
|||||||
_resetIndex.ResolveVariables();
|
_resetIndex.ResolveVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MacroActionSequence::SetAction(Action action)
|
||||||
|
{
|
||||||
|
_action = action;
|
||||||
|
SetupTempVars();
|
||||||
|
}
|
||||||
|
|
||||||
bool MacroActionSequence::RunSequence()
|
bool MacroActionSequence::RunSequence()
|
||||||
{
|
{
|
||||||
if (_macros.size() == 0) {
|
if (_macros.size() == 0) {
|
||||||
|
SetTempVarValue("macro", "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto macro = GetNextMacro().GetMacro();
|
auto macro = GetNextMacro().GetMacro();
|
||||||
|
SetTempVarValue("macro", GetMacroName(macro.get()));
|
||||||
|
|
||||||
if (!macro.get()) {
|
if (!macro.get()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -96,18 +105,21 @@ bool MacroActionSequence::RunSequence()
|
|||||||
return RunMacroActions(macro.get());
|
return RunMacroActions(macro.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MacroActionSequence::SetSequenceIndex() const
|
bool MacroActionSequence::SetSequenceIndex()
|
||||||
{
|
{
|
||||||
auto macro = _macro.GetMacro();
|
auto macro = _macro.GetMacro();
|
||||||
if (!macro) {
|
if (!macro) {
|
||||||
|
SetTempVarValue("nextMacro", "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto actions = GetMacroActions(macro.get());
|
auto actions = GetMacroActions(macro.get());
|
||||||
if (!actions) {
|
if (!actions) {
|
||||||
|
SetTempVarValue("nextMacro", "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string nextMacroName;
|
||||||
for (const auto &action : *actions) {
|
for (const auto &action : *actions) {
|
||||||
if (action->GetId() != id) {
|
if (action->GetId() != id) {
|
||||||
continue;
|
continue;
|
||||||
@@ -121,10 +133,34 @@ bool MacroActionSequence::SetSequenceIndex() const
|
|||||||
// -2 is needed since the _lastIndex starts at -1 and the reset
|
// -2 is needed since the _lastIndex starts at -1 and the reset
|
||||||
// index starts at 1
|
// index starts at 1
|
||||||
sequenceAction->_lastIdx = _resetIndex - 2;
|
sequenceAction->_lastIdx = _resetIndex - 2;
|
||||||
|
nextMacroName = GetMacroName(
|
||||||
|
sequenceAction->GetNextMacro(false).GetMacro().get());
|
||||||
}
|
}
|
||||||
|
SetTempVarValue("nextMacro", nextMacroName);
|
||||||
return true;
|
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()
|
bool MacroActionSequence::PerformAction()
|
||||||
{
|
{
|
||||||
if (_action == Action::RUN_SEQUENCE) {
|
if (_action == Action::RUN_SEQUENCE) {
|
||||||
@@ -162,7 +198,7 @@ bool MacroActionSequence::Load(obs_data_t *obj)
|
|||||||
LoadMacroList(obj, _macros);
|
LoadMacroList(obj, _macros);
|
||||||
_restart = obs_data_get_bool(obj, "restart");
|
_restart = obs_data_get_bool(obj, "restart");
|
||||||
_macro.Load(obj);
|
_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");
|
_resetIndex.Load(obj, "resetIndex");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -256,7 +292,7 @@ void MacroActionSequenceEdit::UpdateEntryData()
|
|||||||
_macroList->SetContent(_entryData->_macros);
|
_macroList->SetContent(_entryData->_macros);
|
||||||
_restart->setChecked(_entryData->_restart);
|
_restart->setChecked(_entryData->_restart);
|
||||||
_resetIndex->SetValue(_entryData->_resetIndex);
|
_resetIndex->SetValue(_entryData->_resetIndex);
|
||||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
|
||||||
_macros->SetCurrentMacro(_entryData->_macro);
|
_macros->SetCurrentMacro(_entryData->_macro);
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
adjustSize();
|
adjustSize();
|
||||||
@@ -361,7 +397,7 @@ void MacroActionSequenceEdit::UpdateStatusLine()
|
|||||||
void MacroActionSequenceEdit::ActionChanged(int value)
|
void MacroActionSequenceEdit::ActionChanged(int value)
|
||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_action = static_cast<MacroActionSequence::Action>(value);
|
_entryData->SetAction(static_cast<MacroActionSequence::Action>(value));
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +421,10 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
|
|||||||
|
|
||||||
ClearLayout(_layout);
|
ClearLayout(_layout);
|
||||||
|
|
||||||
|
const auto action = _entryData->GetAction();
|
||||||
PlaceWidgets(
|
PlaceWidgets(
|
||||||
obs_module_text(
|
obs_module_text(
|
||||||
_entryData->_action ==
|
action == MacroActionSequence::Action::RUN_SEQUENCE
|
||||||
MacroActionSequence::Action::RUN_SEQUENCE
|
|
||||||
? "AdvSceneSwitcher.action.sequence.entry.run"
|
? "AdvSceneSwitcher.action.sequence.entry.run"
|
||||||
: "AdvSceneSwitcher.action.sequence.entry.setIndex"),
|
: "AdvSceneSwitcher.action.sequence.entry.setIndex"),
|
||||||
_layout,
|
_layout,
|
||||||
@@ -396,15 +432,14 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
|
|||||||
{"{{macros}}", _macros},
|
{"{{macros}}", _macros},
|
||||||
{"{{index}}", _resetIndex}});
|
{"{{index}}", _resetIndex}});
|
||||||
|
|
||||||
_macroList->setVisible(_entryData->_action ==
|
_macroList->setVisible(action ==
|
||||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||||
_restart->setVisible(_entryData->_action ==
|
_restart->setVisible(action ==
|
||||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||||
_statusLine->setVisible(_entryData->_action ==
|
_statusLine->setVisible(action ==
|
||||||
MacroActionSequence::Action::RUN_SEQUENCE);
|
MacroActionSequence::Action::RUN_SEQUENCE);
|
||||||
_macros->setVisible(_entryData->_action ==
|
_macros->setVisible(action == MacroActionSequence::Action::SET_INDEX);
|
||||||
MacroActionSequence::Action::SET_INDEX);
|
_resetIndex->setVisible(action ==
|
||||||
_resetIndex->setVisible(_entryData->_action ==
|
|
||||||
MacroActionSequence::Action::SET_INDEX);
|
MacroActionSequence::Action::SET_INDEX);
|
||||||
|
|
||||||
adjustSize();
|
adjustSize();
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ public:
|
|||||||
RUN_SEQUENCE,
|
RUN_SEQUENCE,
|
||||||
SET_INDEX,
|
SET_INDEX,
|
||||||
};
|
};
|
||||||
Action _action = Action::RUN_SEQUENCE;
|
|
||||||
|
Action GetAction() const { return _action; }
|
||||||
|
void SetAction(Action);
|
||||||
|
|
||||||
bool _restart = true;
|
bool _restart = true;
|
||||||
IntVariable _resetIndex = 1;
|
IntVariable _resetIndex = 1;
|
||||||
|
|
||||||
@@ -43,7 +46,11 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool RunSequence();
|
bool RunSequence();
|
||||||
bool SetSequenceIndex() const;
|
bool SetSequenceIndex();
|
||||||
|
|
||||||
|
void SetupTempVars();
|
||||||
|
|
||||||
|
Action _action = Action::RUN_SEQUENCE;
|
||||||
|
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
static const std::string id;
|
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";
|
const std::string MacroActionSource::id = "source";
|
||||||
|
|
||||||
|
std::vector<TempVariableRef> MacroActionSource::GetTempVarRefs() const
|
||||||
|
{
|
||||||
|
if (!_tempVar.HasValidID()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {_tempVar};
|
||||||
|
}
|
||||||
|
|
||||||
bool MacroActionSource::_registered = MacroActionFactory::Register(
|
bool MacroActionSource::_registered = MacroActionFactory::Register(
|
||||||
MacroActionSource::id,
|
MacroActionSource::id,
|
||||||
{MacroActionSource::Create, MacroActionSourceEdit::Create,
|
{MacroActionSource::Create, MacroActionSourceEdit::Create,
|
||||||
@@ -45,6 +53,10 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
|
|||||||
"AdvSceneSwitcher.action.source.type.closeFilterDialog"},
|
"AdvSceneSwitcher.action.source.type.closeFilterDialog"},
|
||||||
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
|
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
|
||||||
"AdvSceneSwitcher.action.source.type.closePropertiesDialog"},
|
"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 = {
|
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
|
||||||
@@ -273,6 +285,22 @@ bool MacroActionSource::PerformAction()
|
|||||||
"OBSBasicProperties");
|
"OBSBasicProperties");
|
||||||
});
|
});
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -360,6 +388,31 @@ void MacroActionSource::ResolveVariablesToFixedValues()
|
|||||||
_manualSettingValue.ResolveVariables();
|
_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)
|
static inline void populateActionSelection(QComboBox *list)
|
||||||
{
|
{
|
||||||
for (auto &[actionType, name] : actionTypes) {
|
for (auto &[actionType, name] : actionTypes) {
|
||||||
@@ -502,7 +555,7 @@ void MacroActionSourceEdit::UpdateEntryData()
|
|||||||
|
|
||||||
const auto weakSource = _entryData->_source.GetSource();
|
const auto weakSource = _entryData->_source.GetSource();
|
||||||
_settingsButtons->SetSelection(weakSource, _entryData->_button);
|
_settingsButtons->SetSelection(weakSource, _entryData->_button);
|
||||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
|
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
|
||||||
_sources->SetSource(_entryData->_source);
|
_sources->SetSource(_entryData->_source);
|
||||||
_sourceSettings->SetSelection(weakSource, _entryData->_setting);
|
_sourceSettings->SetSelection(weakSource, _entryData->_setting);
|
||||||
_settingsString->setPlainText(_entryData->_settingsString);
|
_settingsString->setPlainText(_entryData->_settingsString);
|
||||||
@@ -537,7 +590,7 @@ void MacroActionSourceEdit::SourceChanged(const SourceSelection &source)
|
|||||||
void MacroActionSourceEdit::ActionChanged(int value)
|
void MacroActionSourceEdit::ActionChanged(int value)
|
||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_action = static_cast<MacroActionSource::Action>(value);
|
_entryData->SetAction(static_cast<MacroActionSource::Action>(value));
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +660,7 @@ void MacroActionSourceEdit::SelectionChanged(const TempVariableRef &var)
|
|||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_tempVar = var;
|
_entryData->_tempVar = var;
|
||||||
|
IncrementTempVarInUseGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
|
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
|
||||||
@@ -664,19 +718,29 @@ static QString GetIndividualListEntryName()
|
|||||||
|
|
||||||
void MacroActionSourceEdit::SetWidgetVisibility()
|
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,
|
SetLayoutVisible(_settingsLayout,
|
||||||
_entryData->_action ==
|
isSetSettings || isGetSetting || isGetSettings);
|
||||||
MacroActionSource::Action::SETTINGS);
|
_settingsInputMethods->setVisible(isSetSettings);
|
||||||
_sourceSettings->setVisible(
|
_sourceSettings->setVisible(
|
||||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
(isSetSettings &&
|
||||||
_entryData->_settingsInputMethod !=
|
_entryData->_settingsInputMethod !=
|
||||||
MacroActionSource::SettingsInputMethod::JSON_STRING);
|
MacroActionSource::SettingsInputMethod::JSON_STRING) ||
|
||||||
|
isGetSetting);
|
||||||
_settingsString->setVisible(
|
_settingsString->setVisible(
|
||||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
isSetSettings &&
|
||||||
_entryData->_settingsInputMethod ==
|
_entryData->_settingsInputMethod ==
|
||||||
MacroActionSource::SettingsInputMethod::JSON_STRING);
|
MacroActionSource::SettingsInputMethod::JSON_STRING);
|
||||||
_getSettings->setVisible(
|
_getSettings->setVisible(
|
||||||
_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
isSetSettings &&
|
||||||
_entryData->_settingsInputMethod !=
|
_entryData->_settingsInputMethod !=
|
||||||
MacroActionSource::SettingsInputMethod::
|
MacroActionSource::SettingsInputMethod::
|
||||||
INDIVIDUAL_TEMPVAR);
|
INDIVIDUAL_TEMPVAR);
|
||||||
@@ -685,13 +749,12 @@ void MacroActionSourceEdit::SetWidgetVisibility()
|
|||||||
GetIndividualListEntryName(),
|
GetIndividualListEntryName(),
|
||||||
_entryData->_setting.IsList());
|
_entryData->_setting.IsList());
|
||||||
|
|
||||||
_tempVars->setVisible(_entryData->_action ==
|
_tempVars->setVisible(isSetSettings &&
|
||||||
MacroActionSource::Action::SETTINGS &&
|
|
||||||
_entryData->_settingsInputMethod ==
|
_entryData->_settingsInputMethod ==
|
||||||
MacroActionSource::SettingsInputMethod::
|
MacroActionSource::SettingsInputMethod::
|
||||||
INDIVIDUAL_TEMPVAR);
|
INDIVIDUAL_TEMPVAR);
|
||||||
|
|
||||||
if (_entryData->_action == MacroActionSource::Action::SETTINGS &&
|
if (isSetSettings &&
|
||||||
(_entryData->_settingsInputMethod ==
|
(_entryData->_settingsInputMethod ==
|
||||||
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL ||
|
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL ||
|
||||||
_entryData->_settingsInputMethod ==
|
_entryData->_settingsInputMethod ==
|
||||||
@@ -704,35 +767,31 @@ void MacroActionSourceEdit::SetWidgetVisibility()
|
|||||||
_manualSettingValue->hide();
|
_manualSettingValue->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool showWarning =
|
const bool showWarning = action == MacroActionSource::Action::ENABLE ||
|
||||||
_entryData->_action == MacroActionSource::Action::ENABLE ||
|
action == MacroActionSource::Action::DISABLE;
|
||||||
_entryData->_action == MacroActionSource::Action::DISABLE;
|
|
||||||
_warning->setVisible(showWarning);
|
_warning->setVisible(showWarning);
|
||||||
_settingsButtons->setVisible(
|
_settingsButtons->setVisible(
|
||||||
_entryData->_action ==
|
action == MacroActionSource::Action::SETTINGS_BUTTON);
|
||||||
MacroActionSource::Action::SETTINGS_BUTTON);
|
|
||||||
_deinterlaceMode->setVisible(
|
_deinterlaceMode->setVisible(
|
||||||
_entryData->_action ==
|
action == MacroActionSource::Action::DEINTERLACE_MODE);
|
||||||
MacroActionSource::Action::DEINTERLACE_MODE);
|
|
||||||
_deinterlaceOrder->setVisible(
|
_deinterlaceOrder->setVisible(
|
||||||
_entryData->_action ==
|
action == MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
|
||||||
MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
|
|
||||||
|
|
||||||
_refreshSettingSelection->setVisible(
|
_refreshSettingSelection->setVisible(
|
||||||
(_entryData->_settingsInputMethod ==
|
((isSetSettings &&
|
||||||
MacroActionSource::SettingsInputMethod::
|
(_entryData->_settingsInputMethod ==
|
||||||
INDIVIDUAL_MANUAL ||
|
MacroActionSource::SettingsInputMethod::
|
||||||
_entryData->_settingsInputMethod ==
|
INDIVIDUAL_MANUAL ||
|
||||||
MacroActionSource::SettingsInputMethod::
|
_entryData->_settingsInputMethod ==
|
||||||
INDIVIDUAL_LIST_ENTRY) &&
|
MacroActionSource::SettingsInputMethod::
|
||||||
|
INDIVIDUAL_LIST_ENTRY)) ||
|
||||||
|
isGetSetting) &&
|
||||||
_entryData->_source.GetType() ==
|
_entryData->_source.GetType() ==
|
||||||
SourceSelection::Type::VARIABLE);
|
SourceSelection::Type::VARIABLE);
|
||||||
|
|
||||||
_acceptDialog->setVisible(
|
_acceptDialog->setVisible(
|
||||||
_entryData->_action ==
|
action == MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
|
||||||
MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
|
action == MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
|
||||||
_entryData->_action ==
|
|
||||||
MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
|
|
||||||
|
|
||||||
adjustSize();
|
adjustSize();
|
||||||
updateGeometry();
|
updateGeometry();
|
||||||
|
|||||||
@@ -15,26 +15,22 @@ namespace advss {
|
|||||||
class MacroActionSource : public MacroAction {
|
class MacroActionSource : public MacroAction {
|
||||||
public:
|
public:
|
||||||
MacroActionSource(Macro *m) : MacroAction(m) {}
|
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);
|
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||||
std::shared_ptr<MacroAction> Copy() const;
|
std::shared_ptr<MacroAction> Copy() const;
|
||||||
void ResolveVariablesToFixedValues();
|
|
||||||
|
|
||||||
SourceSelection _source;
|
bool PerformAction();
|
||||||
SourceSettingButton _button;
|
void LogAction() const;
|
||||||
StringVariable _settingsString = "";
|
|
||||||
StringVariable _manualSettingValue = "";
|
bool Save(obs_data_t *obj) const;
|
||||||
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
|
bool Load(obs_data_t *obj);
|
||||||
obs_deinterlace_field_order _deinterlaceOrder =
|
|
||||||
OBS_DEINTERLACE_FIELD_ORDER_TOP;
|
std::string GetShortDesc() const;
|
||||||
TempVariableRef _tempVar;
|
std::string GetId() const { return id; };
|
||||||
SourceSetting _setting;
|
|
||||||
bool _acceptDialog = false;
|
void ResolveVariablesToFixedValues();
|
||||||
|
void SetupTempVars();
|
||||||
|
std::vector<TempVariableRef> GetTempVarRefs() const;
|
||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
ENABLE,
|
ENABLE,
|
||||||
@@ -50,8 +46,23 @@ public:
|
|||||||
CLOSE_INTERACTION_DIALOG,
|
CLOSE_INTERACTION_DIALOG,
|
||||||
CLOSE_FILTER_DIALOG,
|
CLOSE_FILTER_DIALOG,
|
||||||
CLOSE_PROPERTIES_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 {
|
enum class SettingsInputMethod {
|
||||||
INDIVIDUAL_MANUAL,
|
INDIVIDUAL_MANUAL,
|
||||||
@@ -63,6 +74,8 @@ public:
|
|||||||
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
SettingsInputMethod::INDIVIDUAL_MANUAL;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
Action _action = Action::SETTINGS;
|
||||||
|
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
static const std::string id;
|
static const std::string id;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "transition-helpers.hpp"
|
#include "transition-helpers.hpp"
|
||||||
|
|
||||||
#include <obs-frontend-api.h>
|
#include <obs-frontend-api.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ const static std::map<MacroActionTransition::Type, std::string> actionTypes = {
|
|||||||
"AdvSceneSwitcher.action.transition.type.sourceShow"},
|
"AdvSceneSwitcher.action.transition.type.sourceShow"},
|
||||||
{MacroActionTransition::Type::SOURCE_HIDE,
|
{MacroActionTransition::Type::SOURCE_HIDE,
|
||||||
"AdvSceneSwitcher.action.transition.type.sourceHide"},
|
"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()
|
void MacroActionTransition::SetSceneTransition()
|
||||||
@@ -76,6 +81,13 @@ static void obs_sceneitem_set_transition_duration(obs_sceneitem_t *item,
|
|||||||
}
|
}
|
||||||
#endif
|
#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)
|
void MacroActionTransition::SetSourceTransition(bool show)
|
||||||
{
|
{
|
||||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0)
|
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0)
|
||||||
@@ -110,6 +122,12 @@ bool MacroActionTransition::PerformAction()
|
|||||||
case Type::SOURCE_HIDE:
|
case Type::SOURCE_HIDE:
|
||||||
SetSourceTransition(false);
|
SetSourceTransition(false);
|
||||||
break;
|
break;
|
||||||
|
case Type::TBAR:
|
||||||
|
SetTbarPosition();
|
||||||
|
break;
|
||||||
|
case Type::RELEASE_TBAR:
|
||||||
|
obs_frontend_release_tbar();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -119,22 +137,29 @@ void MacroActionTransition::LogAction() const
|
|||||||
std::string msgBegin;
|
std::string msgBegin;
|
||||||
switch (_type) {
|
switch (_type) {
|
||||||
case Type::SCENE:
|
case Type::SCENE:
|
||||||
msgBegin += "set scene transition";
|
msgBegin = "set scene transition";
|
||||||
break;
|
break;
|
||||||
case Type::SCENE_OVERRIDE:
|
case Type::SCENE_OVERRIDE:
|
||||||
msgBegin += "set scene override transition of " +
|
msgBegin = "set scene override transition of " +
|
||||||
_scene.ToString(true);
|
_scene.ToString(true);
|
||||||
break;
|
break;
|
||||||
case Type::SOURCE_SHOW:
|
case Type::SOURCE_SHOW:
|
||||||
msgBegin += "set source show transition of " +
|
msgBegin = "set source show transition of " +
|
||||||
_source.ToString(true) + " on scene " +
|
_source.ToString(true) + " on scene " +
|
||||||
_scene.ToString(true);
|
_scene.ToString(true);
|
||||||
break;
|
break;
|
||||||
case Type::SOURCE_HIDE:
|
case Type::SOURCE_HIDE:
|
||||||
msgBegin += "set source hide transition of " +
|
msgBegin = "set source hide transition of " +
|
||||||
_source.ToString(true) + " on scene " +
|
_source.ToString(true) + " on scene " +
|
||||||
_scene.ToString(true);
|
_scene.ToString(true);
|
||||||
break;
|
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) {
|
if (_setDuration) {
|
||||||
ablog(LOG_INFO, "%s duration to %s", msgBegin.c_str(),
|
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);
|
_transition.Save(obj);
|
||||||
obs_data_set_bool(obj, "setDuration", _setDuration);
|
obs_data_set_bool(obj, "setDuration", _setDuration);
|
||||||
obs_data_set_bool(obj, "setType", _setTransitionType);
|
obs_data_set_bool(obj, "setType", _setTransitionType);
|
||||||
|
_tbarPosition.Save(obj, "tbarPosition");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,23 +195,25 @@ bool MacroActionTransition::Load(obs_data_t *obj)
|
|||||||
_transition.Load(obj);
|
_transition.Load(obj);
|
||||||
_setDuration = obs_data_get_bool(obj, "setDuration");
|
_setDuration = obs_data_get_bool(obj, "setDuration");
|
||||||
_setTransitionType = obs_data_get_bool(obj, "setType");
|
_setTransitionType = obs_data_get_bool(obj, "setType");
|
||||||
|
_tbarPosition.Load(obj, "tbarPosition");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MacroActionTransition::GetShortDesc() const
|
std::string MacroActionTransition::GetShortDesc() const
|
||||||
{
|
{
|
||||||
std::string msgBegin;
|
|
||||||
switch (_type) {
|
switch (_type) {
|
||||||
case Type::SCENE:
|
case Type::SCENE:
|
||||||
return _transition.ToString();
|
return _transition.ToString();
|
||||||
case Type::SCENE_OVERRIDE:
|
case Type::SCENE_OVERRIDE:
|
||||||
return _scene.ToString() + " - " + _transition.ToString();
|
return _scene.ToString() + " - " + _transition.ToString();
|
||||||
case Type::SOURCE_SHOW:
|
case Type::SOURCE_SHOW:
|
||||||
return _scene.ToString() + " - " + _source.ToString() + " - " +
|
|
||||||
_transition.ToString();
|
|
||||||
case Type::SOURCE_HIDE:
|
case Type::SOURCE_HIDE:
|
||||||
return _scene.ToString() + " - " + _source.ToString() + " - " +
|
return _scene.ToString() + " - " + _source.ToString() + " - " +
|
||||||
_transition.ToString();
|
_transition.ToString();
|
||||||
|
case Type::TBAR:
|
||||||
|
return std::to_string(_tbarPosition.GetValue()) + "%";
|
||||||
|
case Type::RELEASE_TBAR:
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -205,6 +233,7 @@ void MacroActionTransition::ResolveVariablesToFixedValues()
|
|||||||
_source.ResolveVariables();
|
_source.ResolveVariables();
|
||||||
_scene.ResolveVariables();
|
_scene.ResolveVariables();
|
||||||
_duration.ResolveVariables();
|
_duration.ResolveVariables();
|
||||||
|
_tbarPosition.ResolveVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void populateActionSelection(QComboBox *list)
|
static inline void populateActionSelection(QComboBox *list)
|
||||||
@@ -236,9 +265,16 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
|||||||
_setDuration(new QCheckBox),
|
_setDuration(new QCheckBox),
|
||||||
_transitions(new TransitionSelectionWidget(this, false)),
|
_transitions(new TransitionSelectionWidget(this, false)),
|
||||||
_duration(new DurationSelection(this, false)),
|
_duration(new DurationSelection(this, false)),
|
||||||
|
_tbarPosition(new VariableDoubleSpinBox),
|
||||||
_transitionLayout(new QHBoxLayout),
|
_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);
|
populateActionSelection(_actions);
|
||||||
|
|
||||||
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
|
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
|
||||||
@@ -260,6 +296,11 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
|||||||
SLOT(SetTransitionChanged(int)));
|
SLOT(SetTransitionChanged(int)));
|
||||||
QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this,
|
QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this,
|
||||||
SLOT(SetDurationChanged(int)));
|
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 = {
|
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||||
{"{{type}}", _actions},
|
{"{{type}}", _actions},
|
||||||
@@ -269,22 +310,29 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
|
|||||||
{"{{duration}}", _duration},
|
{"{{duration}}", _duration},
|
||||||
{"{{setTransition}}", _setTransition},
|
{"{{setTransition}}", _setTransition},
|
||||||
{"{{setDuration}}", _setDuration},
|
{"{{setDuration}}", _setDuration},
|
||||||
|
{"{{tbarPosition}}", _tbarPosition},
|
||||||
};
|
};
|
||||||
|
|
||||||
auto typeLayout = new QHBoxLayout;
|
auto typeLayout = new QHBoxLayout;
|
||||||
PlaceWidgets(obs_module_text(
|
PlaceWidgets(obs_module_text(
|
||||||
"AdvSceneSwitcher.action.transition.entry.line1"),
|
"AdvSceneSwitcher.action.transition.layout.type"),
|
||||||
typeLayout, widgetPlaceholders);
|
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(
|
PlaceWidgets(obs_module_text(
|
||||||
"AdvSceneSwitcher.action.transition.entry.line2"),
|
"AdvSceneSwitcher.action.transition.layout.tbar"),
|
||||||
_transitionLayout, widgetPlaceholders);
|
_tbarLayout, widgetPlaceholders);
|
||||||
PlaceWidgets(obs_module_text(
|
|
||||||
"AdvSceneSwitcher.action.transition.entry.line3"),
|
|
||||||
_durationLayout, widgetPlaceholders);
|
|
||||||
auto mainLayout = new QVBoxLayout;
|
auto mainLayout = new QVBoxLayout;
|
||||||
mainLayout->addLayout(typeLayout);
|
mainLayout->addLayout(typeLayout);
|
||||||
mainLayout->addLayout(_transitionLayout);
|
mainLayout->addLayout(_transitionLayout);
|
||||||
mainLayout->addLayout(_durationLayout);
|
mainLayout->addLayout(_durationLayout);
|
||||||
|
mainLayout->addLayout(_tbarLayout);
|
||||||
setLayout(mainLayout);
|
setLayout(mainLayout);
|
||||||
|
|
||||||
_entryData = entryData;
|
_entryData = entryData;
|
||||||
@@ -300,13 +348,14 @@ void MacroActionTransitionEdit::UpdateEntryData()
|
|||||||
|
|
||||||
_actions->setCurrentIndex(static_cast<int>(_entryData->_type));
|
_actions->setCurrentIndex(static_cast<int>(_entryData->_type));
|
||||||
_scenes->SetScene(_entryData->_scene);
|
_scenes->SetScene(_entryData->_scene);
|
||||||
_sources->SetSceneItem((_entryData->_source));
|
_sources->SetSceneItem(_entryData->_source);
|
||||||
_setDuration->setChecked(_entryData->_setDuration);
|
_setDuration->setChecked(_entryData->_setDuration);
|
||||||
_duration->SetDuration(_entryData->_duration);
|
_duration->SetDuration(_entryData->_duration);
|
||||||
_setTransition->setChecked(_entryData->_setTransitionType);
|
_setTransition->setChecked(_entryData->_setTransitionType);
|
||||||
_transitions->SetTransition(_entryData->_transition);
|
_transitions->SetTransition(_entryData->_transition);
|
||||||
_transitions->setEnabled(_entryData->_setTransitionType);
|
_transitions->setEnabled(_entryData->_setTransitionType);
|
||||||
_duration->setEnabled(_entryData->_setDuration);
|
_duration->setEnabled(_entryData->_setDuration);
|
||||||
|
_tbarPosition->SetValue(_entryData->_tbarPosition);
|
||||||
SetWidgetVisibility();
|
SetWidgetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,11 +402,19 @@ void MacroActionTransitionEdit::DurationChanged(const Duration &dur)
|
|||||||
|
|
||||||
void MacroActionTransitionEdit::SetWidgetVisibility()
|
void MacroActionTransitionEdit::SetWidgetVisibility()
|
||||||
{
|
{
|
||||||
|
const bool isTbar = _entryData->_type ==
|
||||||
|
MacroActionTransition::Type::TBAR;
|
||||||
|
const bool isReleaseTbar = _entryData->_type ==
|
||||||
|
MacroActionTransition::Type::RELEASE_TBAR;
|
||||||
_sources->setVisible(
|
_sources->setVisible(
|
||||||
_entryData->_type == MacroActionTransition::Type::SOURCE_HIDE ||
|
_entryData->_type == MacroActionTransition::Type::SOURCE_HIDE ||
|
||||||
_entryData->_type == MacroActionTransition::Type::SOURCE_SHOW);
|
_entryData->_type == MacroActionTransition::Type::SOURCE_SHOW);
|
||||||
_scenes->setVisible(_entryData->_type !=
|
_scenes->setVisible(_entryData->_type !=
|
||||||
MacroActionTransition::Type::SCENE);
|
MacroActionTransition::Type::SCENE &&
|
||||||
|
!isTbar && !isReleaseTbar);
|
||||||
|
SetLayoutVisible(_transitionLayout, !isTbar && !isReleaseTbar);
|
||||||
|
SetLayoutVisible(_durationLayout, !isTbar && !isReleaseTbar);
|
||||||
|
SetLayoutVisible(_tbarLayout, isTbar);
|
||||||
adjustSize();
|
adjustSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,4 +438,13 @@ void MacroActionTransitionEdit::SetDurationChanged(int state)
|
|||||||
_duration->setEnabled(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
|
} // namespace advss
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "transition-selection.hpp"
|
#include "transition-selection.hpp"
|
||||||
#include "scene-selection.hpp"
|
#include "scene-selection.hpp"
|
||||||
#include "scene-item-selection.hpp"
|
#include "scene-item-selection.hpp"
|
||||||
|
#include "variable-spinbox.hpp"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -28,6 +29,8 @@ public:
|
|||||||
SCENE_OVERRIDE,
|
SCENE_OVERRIDE,
|
||||||
SOURCE_SHOW,
|
SOURCE_SHOW,
|
||||||
SOURCE_HIDE,
|
SOURCE_HIDE,
|
||||||
|
TBAR,
|
||||||
|
RELEASE_TBAR,
|
||||||
};
|
};
|
||||||
|
|
||||||
Type _type = Type::SCENE;
|
Type _type = Type::SCENE;
|
||||||
@@ -37,11 +40,13 @@ public:
|
|||||||
bool _setTransitionType = true;
|
bool _setTransitionType = true;
|
||||||
TransitionSelection _transition;
|
TransitionSelection _transition;
|
||||||
Duration _duration;
|
Duration _duration;
|
||||||
|
DoubleVariable _tbarPosition = 0.0;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetSceneTransition();
|
void SetSceneTransition();
|
||||||
void SetTransitionOverride();
|
void SetTransitionOverride();
|
||||||
void SetSourceTransition(bool);
|
void SetSourceTransition(bool);
|
||||||
|
void SetTbarPosition();
|
||||||
|
|
||||||
static bool _registered;
|
static bool _registered;
|
||||||
static const std::string id;
|
static const std::string id;
|
||||||
@@ -72,6 +77,7 @@ private slots:
|
|||||||
void SetDurationChanged(int state);
|
void SetDurationChanged(int state);
|
||||||
void TransitionChanged(const TransitionSelection &);
|
void TransitionChanged(const TransitionSelection &);
|
||||||
void DurationChanged(const Duration &seconds);
|
void DurationChanged(const Duration &seconds);
|
||||||
|
void TbarPositionChanged(const NumberVariable<double> &);
|
||||||
signals:
|
signals:
|
||||||
void HeaderInfoChanged(const QString &);
|
void HeaderInfoChanged(const QString &);
|
||||||
|
|
||||||
@@ -83,8 +89,10 @@ protected:
|
|||||||
QCheckBox *_setDuration;
|
QCheckBox *_setDuration;
|
||||||
TransitionSelectionWidget *_transitions;
|
TransitionSelectionWidget *_transitions;
|
||||||
DurationSelection *_duration;
|
DurationSelection *_duration;
|
||||||
|
VariableDoubleSpinBox *_tbarPosition;
|
||||||
QHBoxLayout *_transitionLayout;
|
QHBoxLayout *_transitionLayout;
|
||||||
QHBoxLayout *_durationLayout;
|
QHBoxLayout *_durationLayout;
|
||||||
|
QHBoxLayout *_tbarLayout;
|
||||||
std::shared_ptr<MacroActionTransition> _entryData;
|
std::shared_ptr<MacroActionTransition> _entryData;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -57,8 +57,11 @@ bool MacroActionWait::PerformAction()
|
|||||||
std::chrono::milliseconds((int)(sleepDuration * 1000));
|
std::chrono::milliseconds((int)(sleepDuration * 1000));
|
||||||
|
|
||||||
SetMacroAbortWait(false);
|
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();
|
return !MacroWaitShouldAbort();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -710,6 +710,12 @@ void MacroConditionDateEdit::UpdateEntryData()
|
|||||||
SetWidgetStatus();
|
SetWidgetStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MacroConditionDateEdit::showEvent(QShowEvent *event)
|
||||||
|
{
|
||||||
|
const QSignalBlocker b(this);
|
||||||
|
UpdateEntryData();
|
||||||
|
}
|
||||||
|
|
||||||
void MacroConditionDateEdit::SetupSimpleView()
|
void MacroConditionDateEdit::SetupSimpleView()
|
||||||
{
|
{
|
||||||
SetLayoutVisible(_simpleLayout, true);
|
SetLayoutVisible(_simpleLayout, true);
|
||||||
|
|||||||
@@ -107,6 +107,16 @@ signals:
|
|||||||
void HeaderInfoChanged(const QString &);
|
void HeaderInfoChanged(const QString &);
|
||||||
|
|
||||||
protected:
|
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;
|
QComboBox *_weekCondition;
|
||||||
DayOfWeekSelector *_days;
|
DayOfWeekSelector *_days;
|
||||||
QCheckBox *_ignoreWeekTime;
|
QCheckBox *_ignoreWeekTime;
|
||||||
@@ -134,16 +144,9 @@ protected:
|
|||||||
QHBoxLayout *_repeatUpdateLayout;
|
QHBoxLayout *_repeatUpdateLayout;
|
||||||
QHBoxLayout *_patternLayout;
|
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;
|
QTimer _timer;
|
||||||
|
|
||||||
|
std::shared_ptr<MacroConditionDate> _entryData;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#include "macro-condition-file.hpp"
|
#include "macro-condition-file.hpp"
|
||||||
#include "curl-helper.hpp"
|
|
||||||
#include "layout-helpers.hpp"
|
#include "layout-helpers.hpp"
|
||||||
#include "plugin-state-helpers.hpp"
|
#include "plugin-state-helpers.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <regex>
|
#include <QFileInfo>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -19,29 +17,6 @@ bool MacroConditionFile::_registered = MacroConditionFactory::Register(
|
|||||||
|
|
||||||
static std::hash<std::string> strHash;
|
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)
|
void MacroConditionFile::SetCondition(Condition condition)
|
||||||
{
|
{
|
||||||
_condition = condition;
|
_condition = condition;
|
||||||
@@ -50,14 +25,6 @@ void MacroConditionFile::SetCondition(Condition condition)
|
|||||||
|
|
||||||
bool MacroConditionFile::MatchFileContent(QString &filedata)
|
bool MacroConditionFile::MatchFileContent(QString &filedata)
|
||||||
{
|
{
|
||||||
if (_onlyMatchIfChanged) {
|
|
||||||
size_t newHash = strHash(filedata.toUtf8().constData());
|
|
||||||
if (newHash == _lastHash) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_lastHash = newHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_regex.Enabled()) {
|
if (_regex.Enabled()) {
|
||||||
return _regex.Matches(filedata, QString::fromStdString(_text));
|
return _regex.Matches(filedata, QString::fromStdString(_text));
|
||||||
}
|
}
|
||||||
@@ -66,31 +33,13 @@ bool MacroConditionFile::MatchFileContent(QString &filedata)
|
|||||||
return CompareIgnoringLineEnding(text, filedata);
|
return CompareIgnoringLineEnding(text, filedata);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MacroConditionFile::CheckRemoteFileContent()
|
bool MacroConditionFile::CheckFileContent()
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
QFile file(QString::fromStdString(_file));
|
QFile file(QString::fromStdString(_file));
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_useTime) {
|
|
||||||
QDateTime newLastMod = QFileInfo(file).lastModified();
|
|
||||||
if (_lastMod == newLastMod) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_lastMod = newLastMod;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString filedata = QTextStream(&file).readAll();
|
QString filedata = QTextStream(&file).readAll();
|
||||||
SetVariableValue(filedata.toStdString());
|
SetVariableValue(filedata.toStdString());
|
||||||
SetTempVarValue("content", filedata.toStdString());
|
SetTempVarValue("content", filedata.toStdString());
|
||||||
@@ -103,42 +52,30 @@ bool MacroConditionFile::CheckLocalFileContent()
|
|||||||
bool MacroConditionFile::CheckChangeContent()
|
bool MacroConditionFile::CheckChangeContent()
|
||||||
{
|
{
|
||||||
QString filedata;
|
QString filedata;
|
||||||
switch (_fileType) {
|
|
||||||
case FileType::LOCAL: {
|
std::string path = _file;
|
||||||
std::string path = _file;
|
QFile file(QString::fromStdString(path));
|
||||||
QFile file(QString::fromStdString(path));
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
filedata = QTextStream(&file).readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
SetTempVarValue("content", filedata.toStdString());
|
SetTempVarValue("content", filedata.toStdString());
|
||||||
size_t newHash = strHash(filedata.toUtf8().constData());
|
size_t newHash = strHash(filedata.toUtf8().constData());
|
||||||
const bool contentChanged = newHash != _lastHash;
|
const bool contentChanged = !_firstContentCheck &&
|
||||||
|
(newHash != _lastHash);
|
||||||
_lastHash = newHash;
|
_lastHash = newHash;
|
||||||
|
_firstContentCheck = false;
|
||||||
return contentChanged;
|
return contentChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MacroConditionFile::CheckChangeDate()
|
bool MacroConditionFile::CheckChangeDate()
|
||||||
{
|
{
|
||||||
if (_fileType == FileType::REMOTE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(QString::fromStdString(_file));
|
QFile file(QString::fromStdString(_file));
|
||||||
QDateTime newLastMod = QFileInfo(file).lastModified();
|
QDateTime newLastMod = QFileInfo(file).lastModified();
|
||||||
SetVariableValue(newLastMod.toString().toStdString());
|
SetVariableValue(newLastMod.toString().toStdString());
|
||||||
const bool dateChanged = _lastMod != newLastMod;
|
const bool dateChanged = _lastMod.isValid() && (_lastMod != newLastMod);
|
||||||
_lastMod = newLastMod;
|
_lastMod = newLastMod;
|
||||||
SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString());
|
SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString());
|
||||||
return dateChanged;
|
return dateChanged;
|
||||||
@@ -157,10 +94,6 @@ void MacroConditionFile::SetupTempVars()
|
|||||||
"AdvSceneSwitcher.tempVar.file.content"));
|
"AdvSceneSwitcher.tempVar.file.content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fileType == FileType::REMOTE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTempvar(
|
AddTempvar(
|
||||||
"basename",
|
"basename",
|
||||||
obs_module_text("AdvSceneSwitcher.tempVar.file.basename"),
|
obs_module_text("AdvSceneSwitcher.tempVar.file.basename"),
|
||||||
@@ -199,11 +132,7 @@ bool MacroConditionFile::CheckCondition()
|
|||||||
bool ret = false;
|
bool ret = false;
|
||||||
switch (_condition) {
|
switch (_condition) {
|
||||||
case Condition::MATCH:
|
case Condition::MATCH:
|
||||||
if (_fileType == FileType::REMOTE) {
|
ret = CheckFileContent();
|
||||||
ret = CheckRemoteFileContent();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ret = CheckLocalFileContent();
|
|
||||||
break;
|
break;
|
||||||
case Condition::CONTENT_CHANGE:
|
case Condition::CONTENT_CHANGE:
|
||||||
ret = CheckChangeContent();
|
ret = CheckChangeContent();
|
||||||
@@ -264,10 +193,7 @@ bool MacroConditionFile::Save(obs_data_t *obj) const
|
|||||||
_regex.Save(obj);
|
_regex.Save(obj);
|
||||||
_file.Save(obj, "file");
|
_file.Save(obj, "file");
|
||||||
_text.Save(obj, "text");
|
_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_int(obj, "condition", static_cast<int>(_condition));
|
||||||
obs_data_set_bool(obj, "useTime", _useTime);
|
|
||||||
obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,18 +201,10 @@ bool MacroConditionFile::Load(obs_data_t *obj)
|
|||||||
{
|
{
|
||||||
MacroCondition::Load(obj);
|
MacroCondition::Load(obj);
|
||||||
_regex.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");
|
_file.Load(obj, "file");
|
||||||
_text.Load(obj, "text");
|
_text.Load(obj, "text");
|
||||||
_fileType = static_cast<FileType>(obs_data_get_int(obj, "fileType"));
|
|
||||||
SetCondition(
|
SetCondition(
|
||||||
static_cast<Condition>(obs_data_get_int(obj, "condition")));
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,13 +213,6 @@ std::string MacroConditionFile::GetShortDesc() const
|
|||||||
return _file.UnresolvedValue();
|
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)
|
static void populateConditions(QComboBox *list)
|
||||||
{
|
{
|
||||||
list->addItem(
|
list->addItem(
|
||||||
@@ -321,21 +232,13 @@ static void populateConditions(QComboBox *list)
|
|||||||
MacroConditionFileEdit::MacroConditionFileEdit(
|
MacroConditionFileEdit::MacroConditionFileEdit(
|
||||||
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
|
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
_fileTypes(new QComboBox()),
|
|
||||||
_conditions(new QComboBox()),
|
_conditions(new QComboBox()),
|
||||||
_filePath(new FileSelection()),
|
_filePath(new FileSelection()),
|
||||||
_matchText(new VariableTextEdit(this)),
|
_matchText(new VariableTextEdit(this)),
|
||||||
_regex(new RegexConfigWidget(parent)),
|
_regex(new RegexConfigWidget(parent))
|
||||||
_checkModificationDate(new QCheckBox(obs_module_text(
|
|
||||||
"AdvSceneSwitcher.fileTab.checkfileContentTime"))),
|
|
||||||
_checkFileContent(new QCheckBox(
|
|
||||||
obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")))
|
|
||||||
{
|
{
|
||||||
populateFileTypes(_fileTypes);
|
|
||||||
populateConditions(_conditions);
|
populateConditions(_conditions);
|
||||||
|
|
||||||
QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this,
|
|
||||||
SLOT(FileTypeChanged(int)));
|
|
||||||
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
|
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
|
||||||
SLOT(ConditionChanged(int)));
|
SLOT(ConditionChanged(int)));
|
||||||
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
|
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
|
||||||
@@ -345,42 +248,20 @@ MacroConditionFileEdit::MacroConditionFileEdit(
|
|||||||
QWidget::connect(_regex,
|
QWidget::connect(_regex,
|
||||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||||
SLOT(RegexChanged(const RegexConfig &)));
|
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 = {
|
auto widgetLayout = new QHBoxLayout;
|
||||||
{"{{fileType}}", _fileTypes},
|
widgetLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
{"{{conditions}}", _conditions},
|
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.file.layout"),
|
||||||
{"{{filePath}}", _filePath},
|
widgetLayout,
|
||||||
{"{{matchText}}", _matchText},
|
{{"{{conditions}}", _conditions},
|
||||||
{"{{useRegex}}", _regex},
|
{"{{filePath}}", _filePath},
|
||||||
{"{{checkModificationDate}}", _checkModificationDate},
|
{"{{regex}}", _regex}});
|
||||||
{"{{checkFileContent}}", _checkFileContent},
|
|
||||||
};
|
|
||||||
|
|
||||||
QVBoxLayout *mainLayout = new QVBoxLayout;
|
auto layout = new QVBoxLayout;
|
||||||
QHBoxLayout *line1Layout = new QHBoxLayout;
|
layout->addLayout(widgetLayout);
|
||||||
QHBoxLayout *line2Layout = new QHBoxLayout;
|
layout->addWidget(_matchText);
|
||||||
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);
|
|
||||||
|
|
||||||
setLayout(mainLayout);
|
setLayout(layout);
|
||||||
|
|
||||||
_entryData = entryData;
|
_entryData = entryData;
|
||||||
UpdateEntryData();
|
UpdateEntryData();
|
||||||
@@ -393,47 +274,15 @@ void MacroConditionFileEdit::UpdateEntryData()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileTypes->setCurrentIndex(static_cast<int>(_entryData->_fileType));
|
|
||||||
_conditions->setCurrentIndex(
|
_conditions->setCurrentIndex(
|
||||||
static_cast<int>(_entryData->GetCondition()));
|
static_cast<int>(_entryData->GetCondition()));
|
||||||
_filePath->SetPath(_entryData->_file);
|
_filePath->SetPath(_entryData->_file);
|
||||||
_matchText->setPlainText(_entryData->_text);
|
_matchText->setPlainText(_entryData->_text);
|
||||||
_regex->SetRegexConfig(_entryData->_regex);
|
_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();
|
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)
|
void MacroConditionFileEdit::ConditionChanged(int index)
|
||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
@@ -467,18 +316,6 @@ void MacroConditionFileEdit::RegexChanged(const RegexConfig &conf)
|
|||||||
updateGeometry();
|
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()
|
void MacroConditionFileEdit::SetWidgetVisibility()
|
||||||
{
|
{
|
||||||
if (!_entryData) {
|
if (!_entryData) {
|
||||||
@@ -489,20 +326,6 @@ void MacroConditionFileEdit::SetWidgetVisibility()
|
|||||||
MacroConditionFile::Condition::MATCH);
|
MacroConditionFile::Condition::MATCH);
|
||||||
_regex->setVisible(_entryData->GetCondition() ==
|
_regex->setVisible(_entryData->GetCondition() ==
|
||||||
MacroConditionFile::Condition::MATCH);
|
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();
|
adjustSize();
|
||||||
updateGeometry();
|
updateGeometry();
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QCheckBox>
|
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -26,11 +23,6 @@ public:
|
|||||||
return std::make_shared<MacroConditionFile>(m);
|
return std::make_shared<MacroConditionFile>(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FileType {
|
|
||||||
LOCAL,
|
|
||||||
REMOTE,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class Condition {
|
enum class Condition {
|
||||||
MATCH,
|
MATCH,
|
||||||
CONTENT_CHANGE,
|
CONTENT_CHANGE,
|
||||||
@@ -46,15 +38,9 @@ public:
|
|||||||
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
|
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
|
||||||
RegexConfig _regex;
|
RegexConfig _regex;
|
||||||
|
|
||||||
// TODO: Remove in future version
|
|
||||||
bool _useTime = false;
|
|
||||||
bool _onlyMatchIfChanged = false;
|
|
||||||
FileType _fileType = FileType::LOCAL;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool MatchFileContent(QString &filedata);
|
bool MatchFileContent(QString &filedata);
|
||||||
bool CheckRemoteFileContent();
|
bool CheckFileContent();
|
||||||
bool CheckLocalFileContent();
|
|
||||||
bool CheckChangeContent();
|
bool CheckChangeContent();
|
||||||
bool CheckChangeDate();
|
bool CheckChangeDate();
|
||||||
void SetupTempVars();
|
void SetupTempVars();
|
||||||
@@ -62,6 +48,7 @@ private:
|
|||||||
Condition _condition = Condition::MATCH;
|
Condition _condition = Condition::MATCH;
|
||||||
QDateTime _lastMod;
|
QDateTime _lastMod;
|
||||||
size_t _lastHash = 0;
|
size_t _lastHash = 0;
|
||||||
|
bool _firstContentCheck = true;
|
||||||
std::string _lastFile;
|
std::string _lastFile;
|
||||||
std::string _basename;
|
std::string _basename;
|
||||||
std::string _basenameComplete;
|
std::string _basenameComplete;
|
||||||
@@ -92,29 +79,22 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void FileTypeChanged(int index);
|
|
||||||
void ConditionChanged(int index);
|
void ConditionChanged(int index);
|
||||||
void PathChanged(const QString &text);
|
void PathChanged(const QString &text);
|
||||||
void MatchTextChanged();
|
void MatchTextChanged();
|
||||||
void RegexChanged(const RegexConfig &);
|
void RegexChanged(const RegexConfig &);
|
||||||
void CheckModificationDateChanged(int state);
|
|
||||||
void OnlyMatchIfChangedChanged(int state);
|
|
||||||
signals:
|
signals:
|
||||||
void HeaderInfoChanged(const QString &);
|
void HeaderInfoChanged(const QString &);
|
||||||
|
|
||||||
protected:
|
private:
|
||||||
QComboBox *_fileTypes;
|
void SetWidgetVisibility();
|
||||||
|
|
||||||
QComboBox *_conditions;
|
QComboBox *_conditions;
|
||||||
FileSelection *_filePath;
|
FileSelection *_filePath;
|
||||||
VariableTextEdit *_matchText;
|
VariableTextEdit *_matchText;
|
||||||
RegexConfigWidget *_regex;
|
RegexConfigWidget *_regex;
|
||||||
QCheckBox *_checkModificationDate;
|
|
||||||
QCheckBox *_checkFileContent;
|
|
||||||
std::shared_ptr<MacroConditionFile> _entryData;
|
std::shared_ptr<MacroConditionFile> _entryData;
|
||||||
|
|
||||||
private:
|
|
||||||
void SetWidgetVisibility();
|
|
||||||
|
|
||||||
bool _loading = true;
|
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()
|
endif()
|
||||||
|
|
||||||
target_sources(
|
target_sources(
|
||||||
${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp
|
${PROJECT_NAME}
|
||||||
key-value-list.cpp key-value-list.hpp)
|
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})
|
setup_advss_plugin(${PROJECT_NAME})
|
||||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
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()
|
bool MacroActionHttp::PerformAction()
|
||||||
{
|
{
|
||||||
|
// Capture all config while holding the segment lock
|
||||||
const auto [host, path] = getURLInfo(_url, !_setParams);
|
const auto [host, path] = getURLInfo(_url, !_setParams);
|
||||||
|
|
||||||
httplib::Client cli(host);
|
httplib::Client cli(host);
|
||||||
setTimeout(cli, _timeout);
|
setTimeout(cli, _timeout);
|
||||||
const auto params = _setParams ? getParams(_params) : httplib::Params();
|
const auto params = _setParams ? getParams(_params) : httplib::Params();
|
||||||
const auto headers = _setHeaders ? getHeaders(_headers)
|
const auto headers = _setHeaders ? getHeaders(_headers)
|
||||||
: httplib::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;
|
httplib::Result response;
|
||||||
switch (_method) {
|
{
|
||||||
case MacroActionHttp::Method::GET:
|
SuspendLock suspendLock(*this);
|
||||||
response = cli.Get(path, params, headers);
|
switch (method) {
|
||||||
break;
|
case MacroActionHttp::Method::GET:
|
||||||
case MacroActionHttp::Method::POST: {
|
response = cli.Get(path, params, headers);
|
||||||
const auto pathWithParam =
|
break;
|
||||||
httplib::append_query_params(path, params);
|
case MacroActionHttp::Method::POST: {
|
||||||
response =
|
const auto pathWithParam =
|
||||||
cli.Post(pathWithParam, headers, _body, _contentType);
|
httplib::append_query_params(path, params);
|
||||||
break;
|
response = cli.Post(pathWithParam, headers, body,
|
||||||
}
|
contentType);
|
||||||
case MacroActionHttp::Method::PUT: {
|
break;
|
||||||
const auto pathWithParam =
|
}
|
||||||
httplib::append_query_params(path, params);
|
case MacroActionHttp::Method::PUT: {
|
||||||
response = cli.Put(pathWithParam, headers, _body, _contentType);
|
const auto pathWithParam =
|
||||||
break;
|
httplib::append_query_params(path, params);
|
||||||
}
|
response = cli.Put(pathWithParam, headers, body,
|
||||||
case MacroActionHttp::Method::PATCH: {
|
contentType);
|
||||||
const auto pathWithParam =
|
break;
|
||||||
httplib::append_query_params(path, params);
|
}
|
||||||
response =
|
case MacroActionHttp::Method::PATCH: {
|
||||||
cli.Patch(pathWithParam, headers, _body, _contentType);
|
const auto pathWithParam =
|
||||||
break;
|
httplib::append_query_params(path, params);
|
||||||
}
|
response = cli.Patch(pathWithParam, headers, body,
|
||||||
case MacroActionHttp::Method::DELETE: {
|
contentType);
|
||||||
const auto pathWithParam =
|
break;
|
||||||
httplib::append_query_params(path, params);
|
}
|
||||||
response =
|
case MacroActionHttp::Method::DELETE: {
|
||||||
cli.Delete(pathWithParam, headers, _body, _contentType);
|
const auto pathWithParam =
|
||||||
break;
|
httplib::append_query_params(path, params);
|
||||||
}
|
response = cli.Delete(pathWithParam, headers, body,
|
||||||
default:
|
contentType);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VerboseLoggingEnabled() && !response) {
|
if (VerboseLoggingEnabled() && !response) {
|
||||||
@@ -485,10 +493,10 @@ void MacroActionHttpEdit::SetWidgetLayout()
|
|||||||
|
|
||||||
auto layout = new QVBoxLayout;
|
auto layout = new QVBoxLayout;
|
||||||
layout->addLayout(actionLayout);
|
layout->addLayout(actionLayout);
|
||||||
layout->addWidget(_setHeaders);
|
|
||||||
layout->addLayout(_headerListLayout);
|
|
||||||
layout->addWidget(_setParams);
|
layout->addWidget(_setParams);
|
||||||
layout->addLayout(_paramListLayout);
|
layout->addLayout(_paramListLayout);
|
||||||
|
layout->addWidget(_setHeaders);
|
||||||
|
layout->addLayout(_headerListLayout);
|
||||||
layout->addLayout(_contentTypeLayout);
|
layout->addLayout(_contentTypeLayout);
|
||||||
layout->addLayout(_bodyLayout);
|
layout->addLayout(_bodyLayout);
|
||||||
layout->addLayout(timeoutLayout);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SuspendLock suspendLock(static_cast<MacroAction &>(*this));
|
||||||
(void)SendTriggerSignal();
|
(void)SendTriggerSignal();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ bool MacroConditionScript::CheckCondition()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SuspendLock suspendLock(static_cast<MacroCondition &>(*this));
|
||||||
return SendTriggerSignal();
|
return SendTriggerSignal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ MacroSegmentScriptInlineEdit::MacroSegmentScriptInlineEdit(
|
|||||||
_scriptType(new QComboBox(this)),
|
_scriptType(new QComboBox(this)),
|
||||||
_language(new QComboBox(this)),
|
_language(new QComboBox(this)),
|
||||||
_script(new ScriptEditor(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(
|
_openFile(new QPushButton(
|
||||||
obs_module_text("AdvSceneSwitcher.script.file.open"), this)),
|
obs_module_text("AdvSceneSwitcher.script.file.open"), this)),
|
||||||
_fileLayout(new QHBoxLayout()),
|
_fileLayout(new QHBoxLayout()),
|
||||||
|
|||||||
@@ -68,10 +68,15 @@ void ContentClassification::SetContentClassification(
|
|||||||
data = obs_data_create();
|
data = obs_data_create();
|
||||||
obs_data_set_array(data, "content_classification_labels", ccls);
|
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",
|
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||||
"/helix/channels",
|
"/helix/channels",
|
||||||
{{"broadcaster_id", token.GetUserID()}},
|
{{"broadcaster_id", *id}}, data.Get());
|
||||||
data.Get());
|
|
||||||
|
|
||||||
if (result.status != 204) {
|
if (result.status != 204) {
|
||||||
blog(LOG_INFO,
|
blog(LOG_INFO,
|
||||||
|
|||||||
@@ -207,6 +207,26 @@ static obs_data_t *copyData(const OBSData &data)
|
|||||||
return obs_data_create_from_json(json);
|
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,
|
std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
||||||
Subscription subscription)
|
Subscription subscription)
|
||||||
{
|
{
|
||||||
@@ -218,7 +238,12 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
|||||||
|
|
||||||
std::unique_lock<std::mutex> lock(eventSub->_subscriptionMtx);
|
std::unique_lock<std::mutex> lock(eventSub->_subscriptionMtx);
|
||||||
if (!eventSub->_connected) {
|
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());
|
token->GetName().c_str());
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
eventSub->Connect();
|
eventSub->Connect();
|
||||||
@@ -226,9 +251,17 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
|
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
|
||||||
|
eventSub->LogActiveSubscriptions();
|
||||||
return eventSub->_activeSubscriptions.find(subscription)->id;
|
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);
|
OBSDataAutoRelease postData = copyData(subscription.data);
|
||||||
setTransportData(postData.Get(), eventSub->_sessionID);
|
setTransportData(postData.Get(), eventSub->_sessionID);
|
||||||
auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
|
auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
|
||||||
@@ -236,8 +269,14 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
|||||||
postData.Get());
|
postData.Get());
|
||||||
|
|
||||||
if (result.status != 202) {
|
if (result.status != 202) {
|
||||||
vblog(LOG_INFO, "failed to register Twitch EventSub (%d)",
|
const char *error = obs_data_get_string(result.data, "error");
|
||||||
result.status);
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +285,7 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
|
|||||||
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
|
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
|
||||||
subscription.id = obs_data_get_string(replyData, "id");
|
subscription.id = obs_data_get_string(replyData, "id");
|
||||||
eventSub->_activeSubscriptions.emplace(subscription);
|
eventSub->_activeSubscriptions.emplace(subscription);
|
||||||
|
eventSub->LogActiveSubscriptions();
|
||||||
return subscription.id;
|
return subscription.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +442,9 @@ void EventSub::HandleWelcome(obs_data_t *data)
|
|||||||
{
|
{
|
||||||
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
|
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
|
||||||
_sessionID = obs_data_get_string(session, "id");
|
_sessionID = obs_data_get_string(session, "id");
|
||||||
|
ClearActiveSubscriptions();
|
||||||
blog(LOG_INFO, "Twitch EventSub connected");
|
blog(LOG_INFO, "Twitch EventSub connected");
|
||||||
|
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventSub::HandleKeepAlive() const
|
void EventSub::HandleKeepAlive() const
|
||||||
@@ -430,11 +472,14 @@ void EventSub::OnServerMigrationWelcome(
|
|||||||
// Disable reconnect handling for old connection which will be closed
|
// Disable reconnect handling for old connection which will be closed
|
||||||
_client->set_close_handler([](connection_hdl) {});
|
_client->set_close_handler([](connection_hdl) {});
|
||||||
_client->set_fail_handler([](connection_hdl) {});
|
_client->set_fail_handler([](connection_hdl) {});
|
||||||
auto connection = _client->get_con_from_hdl(_connection);
|
if (!_connection.expired()) {
|
||||||
connection->set_close_handler([](connection_hdl) {
|
auto connection = _client->get_con_from_hdl(_connection);
|
||||||
vblog(LOG_INFO, "previous Twitch EventSub connection closed");
|
connection->set_close_handler([](connection_hdl) {
|
||||||
});
|
vblog(LOG_INFO,
|
||||||
connection->set_fail_handler([](connection_hdl) {});
|
"previous Twitch EventSub connection closed");
|
||||||
|
});
|
||||||
|
connection->set_fail_handler([](connection_hdl) {});
|
||||||
|
}
|
||||||
|
|
||||||
websocketpp::lib::error_code ec;
|
websocketpp::lib::error_code ec;
|
||||||
_client->close(_connection, websocketpp::close::status::normal,
|
_client->close(_connection, websocketpp::close::status::normal,
|
||||||
@@ -442,6 +487,7 @@ void EventSub::OnServerMigrationWelcome(
|
|||||||
|
|
||||||
_client.swap(newClient);
|
_client.swap(newClient);
|
||||||
_sessionID = _migrationSessionID;
|
_sessionID = _migrationSessionID;
|
||||||
|
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
|
||||||
_connection = newHdl;
|
_connection = newHdl;
|
||||||
|
|
||||||
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
|
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
|
||||||
@@ -461,17 +507,16 @@ void EventSub::OnServerMigrationWelcome(
|
|||||||
|
|
||||||
void EventSub::StartServerMigrationClient(const std::string &url)
|
void EventSub::StartServerMigrationClient(const std::string &url)
|
||||||
{
|
{
|
||||||
auto client = std::make_unique<EventSubWSClient>();
|
_migrationClient = std::make_unique<EventSubWSClient>();
|
||||||
SetupClient(*client);
|
SetupClient(*_migrationClient);
|
||||||
|
|
||||||
client->set_open_handler([this](connection_hdl) {
|
_migrationClient->set_open_handler([this](connection_hdl) {
|
||||||
vblog(LOG_INFO, "Twitch EventSub migration client opened");
|
vblog(LOG_INFO, "Twitch EventSub migration client opened");
|
||||||
});
|
});
|
||||||
|
|
||||||
client->set_message_handler([this,
|
_migrationClient->set_message_handler([this](connection_hdl hdl,
|
||||||
&client](connection_hdl hdl,
|
EventSubWSClient::message_ptr
|
||||||
EventSubWSClient::message_ptr
|
message) {
|
||||||
message) {
|
|
||||||
const auto msg = ParseWebSocketMessage(message);
|
const auto msg = ParseWebSocketMessage(message);
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return;
|
return;
|
||||||
@@ -487,14 +532,14 @@ void EventSub::StartServerMigrationClient(const std::string &url)
|
|||||||
obs_data_get_obj(data, "session");
|
obs_data_get_obj(data, "session");
|
||||||
_migrationSessionID =
|
_migrationSessionID =
|
||||||
obs_data_get_string(session, "id");
|
obs_data_get_string(session, "id");
|
||||||
OnServerMigrationWelcome(hdl, client);
|
OnServerMigrationWelcome(hdl, _migrationClient);
|
||||||
} else {
|
} else {
|
||||||
OnMessage(hdl, message);
|
OnMessage(hdl, message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
websocketpp::lib::error_code ec;
|
websocketpp::lib::error_code ec;
|
||||||
auto con = client->get_connection(url, ec);
|
auto con = _migrationClient->get_connection(url, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
blog(LOG_ERROR,
|
blog(LOG_ERROR,
|
||||||
"Twitch EventSub migration connection failed: %s",
|
"Twitch EventSub migration connection failed: %s",
|
||||||
@@ -504,8 +549,8 @@ void EventSub::StartServerMigrationClient(const std::string &url)
|
|||||||
}
|
}
|
||||||
|
|
||||||
_migrationConnection = con;
|
_migrationConnection = con;
|
||||||
client->connect(con);
|
_migrationClient->connect(con);
|
||||||
client->run();
|
_migrationClient->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventSub::HandleServerMigration(obs_data_t *data)
|
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);
|
std::lock_guard<std::mutex> lock(_subscriptionMtx);
|
||||||
for (auto it = _activeSubscriptions.begin();
|
for (auto it = _activeSubscriptions.begin();
|
||||||
it != _activeSubscriptions.begin();) {
|
it != _activeSubscriptions.end();) {
|
||||||
if (it->id == id) {
|
if (it->id == id) {
|
||||||
it = _activeSubscriptions.erase(it);
|
it = _activeSubscriptions.erase(it);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ private:
|
|||||||
void HandleServerMigration(obs_data_t *);
|
void HandleServerMigration(obs_data_t *);
|
||||||
void HandleRevocation(obs_data_t *);
|
void HandleRevocation(obs_data_t *);
|
||||||
|
|
||||||
|
void LogActiveSubscriptions() const;
|
||||||
|
|
||||||
void RegisterInstance();
|
void RegisterInstance();
|
||||||
void UnregisterInstance();
|
void UnregisterInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -189,12 +189,17 @@ void LanguageSelection::Save(obs_data_t *obj) const
|
|||||||
|
|
||||||
void LanguageSelection::SetStreamLanguage(const TwitchToken &token) 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();
|
OBSDataAutoRelease data = obs_data_create();
|
||||||
obs_data_set_string(data, "broadcaster_language", _language.c_str());
|
obs_data_set_string(data, "broadcaster_language", _language.c_str());
|
||||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||||
"/helix/channels",
|
"/helix/channels",
|
||||||
{{"broadcaster_id", token.GetUserID()}},
|
{{"broadcaster_id", *id}}, data.Get());
|
||||||
data.Get());
|
|
||||||
|
|
||||||
if (result.status != 204) {
|
if (result.status != 204) {
|
||||||
blog(LOG_INFO, "Failed to set stream language! (%d)",
|
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-line-edit.hpp>
|
||||||
#include <variable-text-edit.hpp>
|
#include <variable-text-edit.hpp>
|
||||||
#include <duration-control.hpp>
|
#include <duration-control.hpp>
|
||||||
|
#include <duration.hpp>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -182,11 +183,13 @@ public:
|
|||||||
StringVariable _announcementMessage = obs_module_text(
|
StringVariable _announcementMessage = obs_module_text(
|
||||||
"AdvSceneSwitcher.action.twitch.announcement.message");
|
"AdvSceneSwitcher.action.twitch.announcement.message");
|
||||||
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
|
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
|
||||||
|
int _nonModDelayDuration = 2;
|
||||||
TwitchChannel _channel;
|
TwitchChannel _channel;
|
||||||
StringVariable _chatMessage;
|
StringVariable _chatMessage;
|
||||||
UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN;
|
UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN;
|
||||||
StringVariable _userLogin = "user login";
|
StringVariable _userLogin = "user login";
|
||||||
DoubleVariable _userId = 0;
|
DoubleVariable _userId = 0;
|
||||||
|
StringVariable _banReason = "";
|
||||||
TwitchPointsReward _pointsReward;
|
TwitchPointsReward _pointsReward;
|
||||||
std::weak_ptr<Variable> _rewardVariable;
|
std::weak_ptr<Variable> _rewardVariable;
|
||||||
bool _useVariableForRewardSelection = false;
|
bool _useVariableForRewardSelection = false;
|
||||||
@@ -198,14 +201,14 @@ private:
|
|||||||
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
|
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
|
||||||
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
|
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
|
||||||
void SendChatAnnouncement(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 StartRaid(const std::shared_ptr<TwitchToken> &);
|
||||||
void SendChatMessage(const std::shared_ptr<TwitchToken> &);
|
void SendChatMessage(const std::shared_ptr<TwitchToken> &);
|
||||||
void GetUserInfo(const std::shared_ptr<TwitchToken> &);
|
void GetUserInfo(const std::shared_ptr<TwitchToken> &);
|
||||||
void GetRewardInfo(const std::shared_ptr<TwitchToken> &);
|
void GetRewardInfo(const std::shared_ptr<TwitchToken> &);
|
||||||
void GetChannelInfo(const std::shared_ptr<TwitchToken> &token);
|
void GetChannelInfo(const std::shared_ptr<TwitchToken> &token);
|
||||||
|
|
||||||
|
std::optional<std::string>
|
||||||
|
GetTargetUserID(const std::shared_ptr<TwitchToken> &) const;
|
||||||
bool ResolveVariableSelectionToRewardId(
|
bool ResolveVariableSelectionToRewardId(
|
||||||
const std::shared_ptr<TwitchToken> &);
|
const std::shared_ptr<TwitchToken> &);
|
||||||
|
|
||||||
@@ -251,11 +254,13 @@ private slots:
|
|||||||
void DurationChanged(const Duration &);
|
void DurationChanged(const Duration &);
|
||||||
void AnnouncementMessageChanged();
|
void AnnouncementMessageChanged();
|
||||||
void AnnouncementColorChanged(int index);
|
void AnnouncementColorChanged(int index);
|
||||||
|
void NonModDelayDurationChanged(int index);
|
||||||
void ChannelChanged(const TwitchChannel &);
|
void ChannelChanged(const TwitchChannel &);
|
||||||
void ChatMessageChanged();
|
void ChatMessageChanged();
|
||||||
void UserInfoQueryTypeChanged(int);
|
void UserInfoQueryTypeChanged(int);
|
||||||
void UserLoginChanged();
|
void UserLoginChanged();
|
||||||
void UserIdChanged(const NumberVariable<double> &);
|
void UserIdChanged(const NumberVariable<double> &);
|
||||||
|
void BanReasonChanged();
|
||||||
void PointsRewardChanged(const TwitchPointsReward &);
|
void PointsRewardChanged(const TwitchPointsReward &);
|
||||||
void RewardVariableChanged(const QString &);
|
void RewardVariableChanged(const QString &);
|
||||||
void ToggleRewardSelection(bool);
|
void ToggleRewardSelection(bool);
|
||||||
@@ -274,6 +279,8 @@ private:
|
|||||||
void SetTokenWarning(bool visible, const QString &text = "");
|
void SetTokenWarning(bool visible, const QString &text = "");
|
||||||
|
|
||||||
QHBoxLayout *_layout;
|
QHBoxLayout *_layout;
|
||||||
|
QWidget *_userModerationRow;
|
||||||
|
QHBoxLayout *_layout2;
|
||||||
FilterComboBox *_actions;
|
FilterComboBox *_actions;
|
||||||
TwitchConnectionSelection *_tokens;
|
TwitchConnectionSelection *_tokens;
|
||||||
QLabel *_tokenWarning;
|
QLabel *_tokenWarning;
|
||||||
@@ -288,6 +295,7 @@ private:
|
|||||||
DurationSelection *_duration;
|
DurationSelection *_duration;
|
||||||
VariableTextEdit *_announcementMessage;
|
VariableTextEdit *_announcementMessage;
|
||||||
QComboBox *_announcementColor;
|
QComboBox *_announcementColor;
|
||||||
|
QComboBox *_nonModDelayDuration;
|
||||||
TwitchChannelSelection *_channel;
|
TwitchChannelSelection *_channel;
|
||||||
VariableTextEdit *_chatMessage;
|
VariableTextEdit *_chatMessage;
|
||||||
QComboBox *_userInfoQueryType;
|
QComboBox *_userInfoQueryType;
|
||||||
@@ -295,6 +303,7 @@ private:
|
|||||||
// QSpinBox uses int internally, which is too small for Twitch IDs, so
|
// QSpinBox uses int internally, which is too small for Twitch IDs, so
|
||||||
// we use QDoubleSpinBox instead
|
// we use QDoubleSpinBox instead
|
||||||
VariableDoubleSpinBox *_userId;
|
VariableDoubleSpinBox *_userId;
|
||||||
|
VariableLineEdit *_banReason;
|
||||||
TwitchPointsRewardWidget *_pointsReward;
|
TwitchPointsRewardWidget *_pointsReward;
|
||||||
VariableSelection *_rewardVariable;
|
VariableSelection *_rewardVariable;
|
||||||
QPushButton *_toggleRewardSelection;
|
QPushButton *_toggleRewardSelection;
|
||||||
|
|||||||
@@ -1096,6 +1096,12 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto id = token->GetUserID();
|
||||||
|
if (!id) {
|
||||||
|
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto channelID = _channel.GetUserID(*token);
|
const auto channelID = _channel.GetUserID(*token);
|
||||||
if (!TwitchChannel::IsValid(channelID)) {
|
if (!TwitchChannel::IsValid(channelID)) {
|
||||||
vblog(LOG_INFO, "skip %s because of invalid channel selection",
|
vblog(LOG_INFO, "skip %s because of invalid channel selection",
|
||||||
@@ -1114,7 +1120,7 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
|
|||||||
|
|
||||||
if (includeModeratorId) {
|
if (includeModeratorId) {
|
||||||
obs_data_set_string(condition, "moderator_user_id",
|
obs_data_set_string(condition, "moderator_user_id",
|
||||||
token->GetUserID().c_str());
|
id->c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
obs_data_apply(condition, extraConditions);
|
obs_data_apply(condition, extraConditions);
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ void TwitchTagList::Save(obs_data_t *obj) const
|
|||||||
|
|
||||||
void TwitchTagList::SetStreamTags(const TwitchToken &token) 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;
|
nlohmann::json j;
|
||||||
j["tags"] = toVector();
|
j["tags"] = toVector();
|
||||||
|
|
||||||
@@ -47,8 +53,7 @@ void TwitchTagList::SetStreamTags(const TwitchToken &token) const
|
|||||||
|
|
||||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||||
"/helix/channels",
|
"/helix/channels",
|
||||||
{{"broadcaster_id", token.GetUserID()}},
|
{{"broadcaster_id", *id}}, j.dump());
|
||||||
j.dump());
|
|
||||||
|
|
||||||
if (result.status != 204) {
|
if (result.status != 204) {
|
||||||
blog(LOG_INFO, "Failed to set stream tags! (%d)",
|
blog(LOG_INFO, "Failed to set stream tags! (%d)",
|
||||||
|
|||||||
@@ -206,7 +206,9 @@ void TwitchToken::Save(obs_data_t *obj) const
|
|||||||
{
|
{
|
||||||
Item::Save(obj);
|
Item::Save(obj);
|
||||||
obs_data_set_string(obj, "token", _token.c_str());
|
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",
|
obs_data_set_bool(obj, "validateEventSubTimestamps",
|
||||||
_validateEventSubTimestamps);
|
_validateEventSubTimestamps);
|
||||||
obs_data_set_bool(obj, "warnIfInvalid", _warnIfInvalid);
|
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");
|
SendGetRequest(*this, "https://api.twitch.tv", "/helix/users");
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
blog(LOG_WARNING, "failed to get Twitch user id from token!");
|
blog(LOG_WARNING, "failed to get Twitch user id from token!");
|
||||||
_userID = -1;
|
_userID = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,12 +320,35 @@ bool TwitchToken::IsValid(bool forceUpdate) const
|
|||||||
cli.Get("/oauth2/validate", httplib::Params{}, headers);
|
cli.Get("/oauth2/validate", httplib::Params{}, headers);
|
||||||
_lastValidityCheckTime = std::chrono::system_clock::now();
|
_lastValidityCheckTime = std::chrono::system_clock::now();
|
||||||
_lastValidityCheckValue = _token;
|
_lastValidityCheckValue = _token;
|
||||||
_lastValidityCheckResult = response && response->status == 200;
|
|
||||||
if (!_lastValidityCheckResult) {
|
if (!response || response->status != 200) {
|
||||||
blog(LOG_INFO, "Twitch token %s is not valid!",
|
blog(LOG_INFO, "Twitch token %s is not valid!",
|
||||||
_name.c_str());
|
_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;
|
const bool tokenChanged = _lastValidityCheckValue != _token;
|
||||||
@@ -729,6 +754,7 @@ void TwitchTokenSettingsDialog::RequestToken()
|
|||||||
|
|
||||||
auto scope = QString::fromStdString(
|
auto scope = QString::fromStdString(
|
||||||
generateScopeString(GetEnabledOptions()));
|
generateScopeString(GetEnabledOptions()));
|
||||||
|
_validationTimer.stop();
|
||||||
_tokenGrabber.SetTokenScope(scope);
|
_tokenGrabber.SetTokenScope(scope);
|
||||||
_tokenGrabber.start();
|
_tokenGrabber.start();
|
||||||
_tokenStatus->setText(obs_module_text(
|
_tokenStatus->setText(obs_module_text(
|
||||||
@@ -766,6 +792,7 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
|
|||||||
Q_ARG(const QString &, name));
|
Q_ARG(const QString &, name));
|
||||||
SetTokenInfoVisible(true);
|
SetTokenInfoVisible(true);
|
||||||
_requestToken->setEnabled(true);
|
_requestToken->setEnabled(true);
|
||||||
|
_validationTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()
|
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
void SetToken(const std::string &);
|
void SetToken(const std::string &);
|
||||||
bool IsEmpty() const { return _token.empty(); }
|
bool IsEmpty() const { return _token.empty(); }
|
||||||
std::optional<std::string> GetToken() const;
|
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();
|
std::shared_ptr<EventSub> GetEventSub();
|
||||||
bool ValidateTimestamps() const { return _validateEventSubTimestamps; }
|
bool ValidateTimestamps() const { return _validateEventSubTimestamps; }
|
||||||
bool IsValid(bool forceUpdate = false) const;
|
bool IsValid(bool forceUpdate = false) const;
|
||||||
@@ -65,7 +65,7 @@ private:
|
|||||||
mutable std::string _lastValidityCheckValue;
|
mutable std::string _lastValidityCheckValue;
|
||||||
mutable bool _lastValidityCheckResult = false;
|
mutable bool _lastValidityCheckResult = false;
|
||||||
mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
|
mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
|
||||||
std::string _userID;
|
std::optional<std::string> _userID;
|
||||||
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
|
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
|
||||||
std::shared_ptr<EventSub> _eventSub;
|
std::shared_ptr<EventSub> _eventSub;
|
||||||
bool _validateEventSubTimestamps = false;
|
bool _validateEventSubTimestamps = false;
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ static RequestResult processResult(const httplib::Result &response,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response->status < 200 || response->status >= 300) {
|
||||||
|
vblog(LOG_INFO, "Twitch API error response: %s",
|
||||||
|
response->body.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
OBSDataAutoRelease replyData =
|
OBSDataAutoRelease replyData =
|
||||||
obs_data_create_from_json(response->body.c_str());
|
obs_data_create_from_json(response->body.c_str());
|
||||||
result.data = replyData;
|
result.data = replyData;
|
||||||
@@ -518,6 +523,13 @@ static RequestResult sendDeleteRequest(const TwitchToken &token,
|
|||||||
return processResult(response, __func__);
|
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,
|
void SetJsonTempVars(const std::string &jsonStr,
|
||||||
std::function<void(const char *, const char *)> setVarFunc)
|
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 httplib::Params ¶ms = {},
|
||||||
const std::string &data = "",
|
const std::string &data = "",
|
||||||
bool useCache = false);
|
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
|
// Helper functions to set temp var values
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,25 @@ static QStringList getCellLabels(TwitchToken *token, bool addName = true)
|
|||||||
return result;
|
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()
|
static void openSettingsDialog()
|
||||||
{
|
{
|
||||||
auto selectedRows =
|
auto selectedRows =
|
||||||
@@ -174,6 +193,7 @@ static void openSettingsDialog()
|
|||||||
|
|
||||||
TwitchTokenSettingsDialog::AskForSettings(GetSettingsWindow(),
|
TwitchTokenSettingsDialog::AskForSettings(GetSettingsWindow(),
|
||||||
*token.get());
|
*token.get());
|
||||||
|
updateConnectionStatus(tabWidget->Table());
|
||||||
}
|
}
|
||||||
|
|
||||||
static const QStringList headers =
|
static const QStringList headers =
|
||||||
@@ -202,25 +222,6 @@ TwitchConnectionsTable::TwitchConnectionsTable(QTabWidget *parent)
|
|||||||
SetHelpVisible(GetTwitchTokens().empty());
|
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()
|
static QStringList getInvalidTokens()
|
||||||
{
|
{
|
||||||
QStringList tokens;
|
QStringList tokens;
|
||||||
|
|||||||
@@ -283,10 +283,18 @@ bool MacroConditionVideo::ScreenshotContainsPattern()
|
|||||||
SetTempVarValue("patternCount", "0");
|
SetTempVarValue("patternCount", "0");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto count = countNonZero(result);
|
|
||||||
SetTempVarValue("similarity", std::to_string(bestMatchValue));
|
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
|
bool MacroConditionVideo::FileInputIsUpToDate() const
|
||||||
@@ -369,13 +377,19 @@ bool MacroConditionVideo::CheckColor()
|
|||||||
_screenshotData.GetImage(), _colorParameters.color,
|
_screenshotData.GetImage(), _colorParameters.color,
|
||||||
_colorParameters.colorThreshold,
|
_colorParameters.colorThreshold,
|
||||||
_colorParameters.matchThreshold);
|
_colorParameters.matchThreshold);
|
||||||
// Way too slow for now
|
|
||||||
//SetTempVarValue("dominantColor", GetDominantColor(_screenshotData.image, 3)
|
SetTempVarValue("color", [&]() {
|
||||||
// .name(QColor::HexArgb)
|
return GetAverageColor(_screenshotData.GetImage())
|
||||||
// .toStdString());
|
.name(QColor::HexArgb)
|
||||||
SetTempVarValue("color", GetAverageColor(_screenshotData.GetImage())
|
.toStdString();
|
||||||
.name(QColor::HexArgb)
|
});
|
||||||
.toStdString());
|
|
||||||
|
SetTempVarValue("dominantColor", [&]() {
|
||||||
|
return GetDominantColor(_screenshotData.GetImage(), 3)
|
||||||
|
.name(QColor::HexArgb)
|
||||||
|
.toStdString();
|
||||||
|
});
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +485,12 @@ void MacroConditionVideo::SetupTempVars()
|
|||||||
obs_module_text("AdvSceneSwitcher.tempVar.video.color"),
|
obs_module_text("AdvSceneSwitcher.tempVar.video.color"),
|
||||||
obs_module_text(
|
obs_module_text(
|
||||||
"AdvSceneSwitcher.tempVar.video.color.description"));
|
"AdvSceneSwitcher.tempVar.video.color.description"));
|
||||||
|
AddTempvar(
|
||||||
|
"dominantColor",
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.tempVar.video.dominantColor"),
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.tempVar.video.dominantColor.description"));
|
||||||
break;
|
break;
|
||||||
case VideoCondition::MATCH:
|
case VideoCondition::MATCH:
|
||||||
case VideoCondition::DIFFER:
|
case VideoCondition::DIFFER:
|
||||||
|
|||||||
@@ -97,6 +97,35 @@ double MatchPattern(QImage &img, const PatternImageData &patternData,
|
|||||||
return bestFitValue;
|
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,
|
double MatchPattern(QImage &img, QImage &pattern, double threshold,
|
||||||
cv::Mat &result, bool useAlphaAsMask,
|
cv::Mat &result, bool useAlphaAsMask,
|
||||||
cv::TemplateMatchModes matchColor)
|
cv::TemplateMatchModes matchColor)
|
||||||
@@ -257,9 +286,9 @@ QColor GetAverageColor(const QImage &img)
|
|||||||
|
|
||||||
auto image = QImageToMat(img);
|
auto image = QImageToMat(img);
|
||||||
cv::Scalar meanColor = cv::mean(image);
|
cv::Scalar meanColor = cv::mean(image);
|
||||||
int averageBlue = cvRound(meanColor[0]);
|
int averageRed = cvRound(meanColor[0]);
|
||||||
int averageGreen = cvRound(meanColor[1]);
|
int averageGreen = cvRound(meanColor[1]);
|
||||||
int averageRed = cvRound(meanColor[2]);
|
int averageBlue = cvRound(meanColor[2]);
|
||||||
|
|
||||||
return QColor(averageRed, averageGreen, averageBlue);
|
return QColor(averageRed, averageGreen, averageBlue);
|
||||||
}
|
}
|
||||||
@@ -280,8 +309,8 @@ QColor GetDominantColor(const QImage &img, int k)
|
|||||||
cv::TermCriteria criteria(
|
cv::TermCriteria criteria(
|
||||||
cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 100, 0.2);
|
cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 100, 0.2);
|
||||||
cv::Mat labels, centers;
|
cv::Mat labels, centers;
|
||||||
cv::kmeans(reshapedImage, k, labels, criteria, 1,
|
cv::kmeans(reshapedImage, k, labels, criteria, 3, cv::KMEANS_PP_CENTERS,
|
||||||
cv::KMEANS_RANDOM_CENTERS, centers);
|
centers);
|
||||||
|
|
||||||
// Find the dominant color
|
// Find the dominant color
|
||||||
// Center of the cluster with the largest number of pixels
|
// 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::Point max_loc;
|
||||||
cv::minMaxLoc(counts, nullptr, nullptr, nullptr, &max_loc);
|
cv::minMaxLoc(counts, nullptr, nullptr, nullptr, &max_loc);
|
||||||
try {
|
const int clusterIdx = max_loc.x;
|
||||||
cv::Scalar dominantColor = centers.at<cv::Scalar>(max_loc.y);
|
const int red =
|
||||||
const int blue = cv::saturate_cast<int>(dominantColor.val[0]);
|
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 0));
|
||||||
const int green = cv::saturate_cast<int>(dominantColor.val[1]);
|
const int green =
|
||||||
const int red = cv::saturate_cast<int>(dominantColor.val[2]);
|
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 1));
|
||||||
const int alpha = cv::saturate_cast<int>(dominantColor.val[3]);
|
const int blue =
|
||||||
return QColor(red, green, blue, alpha);
|
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 2));
|
||||||
} catch (...) {
|
const int alpha =
|
||||||
}
|
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 3));
|
||||||
return QColor();
|
return QColor(red, green, blue, alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assumption is that QImage uses Format_RGBA8888.
|
// 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