~martijnbraam/pyatem

6d6d7da954b2255216b687a3188614af55e62f55 — Martijn Braam 2 months ago 610af29
Remove midi and add macro
M gtk_switcher/atem.gresource.xml => gtk_switcher/atem.gresource.xml +1 -0
@@ 9,6 9,7 @@
    <file>ui/upstream-keyer.glade</file>
    <file>ui/downstream-keyer.glade</file>
    <file>ui/preferences.glade</file>
    <file>ui/macro-editor.glade</file>

    <file>icons/wipe-h.svg</file>
    <file>icons/wipe-v.svg</file>

M gtk_switcher/atemwindow.py => gtk_switcher/atemwindow.py +16 -4
@@ 8,8 8,8 @@ from hexdump import hexdump
from gtk_switcher.audio import AudioPage
from gtk_switcher.camera import CameraPage
from gtk_switcher.decorators import field, call_fields
from gtk_switcher.macroeditor import MacroEditorWindow
from gtk_switcher.media import MediaPage
from gtk_switcher.midi import MidiConnection, MidiControl
from gtk_switcher.connectionwindow import ConnectionWindow
from gtk_switcher.switcher import SwitcherPage
from pyatem.command import ProgramInputCommand, PreviewInputCommand, AutoCommand, TransitionPositionCommand


@@ 84,7 84,7 @@ class AtemConnection(threading.Thread):
            print('Exception raise failure')


class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage):
    def __init__(self, application, args):
        self.application = application
        self.args = args


@@ 119,12 119,12 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
        MediaPage.__init__(self, builder)
        AudioPage.__init__(self, builder)
        CameraPage.__init__(self, builder)
        MidiControl.__init__(self, builder)

        self.status_model = builder.get_object('status_model')
        self.status_mode = builder.get_object('status_mode')
        self.focus_dummy = builder.get_object('focus_dummy')
        self.disable_shortcuts = False
        self.macro_edit = False

        self.apply_css(self.window, self.provider)



@@ 197,6 197,9 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
        self.disable_shortcuts = False
        self.focus_dummy.grab_focus()

    def on_context_menu(self, *args):
        pass

    def on_entry_activate(self, *args):
        self.focus_dummy.grab_focus()



@@ 219,7 222,8 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
        self.connection.die()
        self.connection.join(timeout=1)

        self.connection = AtemConnection(self.on_change, self.on_disconnect, self.on_transfer_progress, self.on_download_done)
        self.connection = AtemConnection(self.on_change, self.on_disconnect, self.on_transfer_progress,
                                         self.on_download_done)
        self.connection.daemon = True
        self.connection.ip = self.settings.get_string('switcher-ip')
        self.connection.start()


@@ 339,6 343,8 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
            self.on_aux_output_source_change(data)
        elif field == 'dkey-properties-base':
            self.on_dkey_properties_base_change(data)
        elif field == 'macro-properties':
            self.on_macro_properties_change(data)
        else:
            if field == 'time':
                return


@@ 358,6 364,12 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage, MidiControl):
        if store == 0:
            # Media transfer
            self.on_media_download_done(slot, data)
        if store == 0xffff:
            # Macro fetch
            if self.macro_edit:
                self.macro_edit = False

                MacroEditorWindow(self.window, self.application, self.connection, slot, data)

    def on_page_changed(self, widget, *args):
        page = widget.get_visible_child_name()

A gtk_switcher/macroeditor.py => gtk_switcher/macroeditor.py +119 -0
@@ 0,0 1,119 @@
import gi

from pyatem.command import MultiviewInputCommand
from pyatem.field import InputPropertiesField
from pyatem.macro import decode_macro, encode_macro, encode_macroscript

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, GObject, Gio, Gdk

gi.require_version('Handy', '1')
from gi.repository import Handy


class MacroEditorWindow:
    def __init__(self, parent, application, connection, index, raw):
        self.application = application
        self.connection = connection

        self.index = index
        self.raw = raw

        builder = Gtk.Builder()
        builder.add_from_resource('/nl/brixit/switcher/ui/macro-editor.glade')
        builder.connect_signals(self)
        css = Gio.resources_lookup_data("/nl/brixit/switcher/ui/style.css", 0)

        self.provider = Gtk.CssProvider()
        self.provider.load_from_data(css.get_data())

        self.window = builder.get_object("window")
        self.headerbar = builder.get_object("headerbar")
        self.main_stack = builder.get_object("main_stack")
        self.sourcecode = builder.get_object("sourcecode")
        self.sourcebuffer = builder.get_object("sourcebuffer")
        self.window.set_application(self.application)

        self.actions = builder.get_object("actions")

        self.apply_css(self.window, self.provider)

        self.window.set_transient_for(parent)
        self.window.set_modal(True)

        macro = self.connection.mixer.mixerstate['macro-properties'][index]
        self.headerbar.set_subtitle(macro.name.decode())

        ma = decode_macro(raw)
        source = encode_macroscript(ma)
        self.sourcebuffer.set_text(source)
        for action in ma:
            frame = Gtk.Frame()
            frame.get_style_context().add_class('view')
            grid = Gtk.Grid()
            grid.set_margin_top(8)
            grid.set_margin_bottom(8)
            grid.set_margin_start(8)
            grid.set_margin_end(8)
            grid.set_column_spacing(8)
            grid.set_row_spacing(8)
            frame.add(grid)
            name = Gtk.Label(action.__class__.NAME)
            name.set_xalign(0.0)
            grid.attach(name, 0, 0, 2, 1)

            top = 1
            for a in action.widgets:
                for attribute, datatype, label, properties in action.widgets[a]:
                    field_label = Gtk.Label(label)
                    field_label.get_style_context().add_class('dim-label')
                    field_label.set_xalign(1.0)
                    grid.attach(field_label, 0, top, 1, 1)

                    widget = None
                    if datatype == 'framecount':
                        widget = Gtk.SpinButton()
                        adjustment = Gtk.Adjustment(getattr(action, attribute), 0, 250, 1, 30, 1)
                        widget.adjustment = adjustment
                        widget.set_adjustment(adjustment)
                    elif datatype == 'number':
                        widget = Gtk.SpinButton()

                        value = getattr(action, attribute)
                        if 'offset' in properties:
                            value += properties['offset']

                        adjustment = Gtk.Adjustment(value, properties['min'], properties['max'], 1, 10, 1)
                        widget.adjustment = adjustment
                        widget.set_adjustment(adjustment)
                    elif datatype == 'source':
                        widget = Gtk.ComboBox()

                    if widget:
                        grid.attach(widget, 1, top, 1, 1)

                    top += 1

            self.actions.add(frame)

        self.window.show_all()

    def apply_css(self, widget, provider):
        Gtk.StyleContext.add_provider(widget.get_style_context(),
                                      provider,
                                      Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

        if isinstance(widget, Gtk.Container):
            widget.forall(self.apply_css, provider)

    def on_cancel_clicked(self, *args):
        self.window.close()

    def on_save_clicked(self, *args):
        self.window.close()

    def on_source_toggled(self, widget, *args):
        if widget.get_active():
            self.main_stack.set_visible_child_name("code")
        else:
            self.main_stack.set_visible_child_name("blocks")

M gtk_switcher/meson.build => gtk_switcher/meson.build +0 -1
@@ 35,7 35,6 @@ switcher_sources = [
    'media.py',
    'audio.py',
    'camera.py',
    'midi.py',
    'mixeffect.py',
    'upstreamkey.py',
    'colorwheel.py',

D gtk_switcher/midi.py => gtk_switcher/midi.py +0 -190
@@ 1,190 0,0 @@
import json
import threading
import time

import gi

gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gtk

has_midi = False
try:
    import rtmidi
    from rtmidi.midiutil import open_midiinput, open_midioutput, list_available_ports

    has_midi = True
except ImportError as e:
    print("Module 'rtmidi' not found, skipping midi support")


class MidiConnection(threading.Thread):
    def __init__(self, port, callback):
        threading.Thread.__init__(self)
        self.callback = callback
        self.port = port
        self.output = None
        self.enabled = False

    def run(self):
        if not has_midi:
            return

        def _midi_in(event, data=None):
            self._do_callback(*event[0])

        if self.port is None:
            temp = rtmidi.MidiIn()
            ports = temp.get_ports()
            for port in ports:
                if 'Midi Through' in port:
                    continue
                break
            else:
                print("No suitable midi port found")
                return
            self.port = port

        midiin, port_name = open_midiinput(self.port, client_name="Switcher")
        midiout, port_name = open_midioutput(self.port, client_name="Switcher")
        self.output = midiout
        midiin.set_callback(_midi_in)
        self.enabled = True

        while True:
            time.sleep(10)

    def _do_callback(self, *args, **kwargs):
        GLib.idle_add(self.callback, *args, **kwargs)

    def send(self, *args):
        if self.enabled:
            self.output.send_message(args)


class MidiLink:
    def __init__(self, widget, key, midi):
        if hasattr(widget, 'dynamic_id'):
            self.name = 'dyn:' + widget.dynamic_id
        else:
            try:
                self.name = Gtk.Buildable.get_name(widget)
            except:
                return
        self.type = None
        self.key = key
        self.midi = midi
        self.widget = widget
        self.adjustment = None
        self.min = None
        self.max = None
        self.is_tbar = False
        self.inverted = False

        if isinstance(widget, gi.repository.Gtk.Scale):
            self.type = 'scale'
            self.adjustment = widget.get_adjustment()
            self.min = self.adjustment.get_lower()
            self.max = self.adjustment.get_upper()
            if hasattr(widget, 'is_tbar') and widget.is_tbar:
                self.is_tbar = True
                self.widget.connect("notify::inverted", self.on_tbar_inverted)
        if isinstance(widget, gi.repository.Gtk.Button):
            self.type = 'button'
            self.widget.connect("style-updated", self.on_style_changed)

        print("New midi mapping: {} <-> {}".format(key, self.name))

    def new_value(self, value):
        if self.type == 'button':
            self.widget.emit("clicked")
        if self.type == 'scale':
            self.widget.tbar_held = True
            if self.inverted:
                self.adjustment.set_value(self._remap(127, 0, self.min, self.max, value))
            else:
                self.adjustment.set_value(self._remap(0, 127, self.min, self.max, value))
            self.widget.tbar_held = False

    def _remap(self, in_min, in_max, out_min, out_max, value):
        in_range = in_max - in_min
        out_range = out_max - out_min
        return (((value - in_min) * out_range) / in_range) + out_min

    def on_tbar_inverted(self, *args):
        self.inverted = not self.inverted

    def on_style_changed(self, *args):
        value = 0
        if self.widget.get_style_context().has_class('active'):
            value = 127
        if self.widget.get_style_context().has_class('program'):
            value = 127
        if self.widget.get_style_context().has_class('preview'):
            value = 127
        self.midi.send(*self.key, value)


class MidiControl:
    def __init__(self, builder):
        self.midi = MidiConnection(None, self.on_midi)
        self.midi.daemon = True
        self.midi.start()

        self.midi_map = {}

        self.menu = None
        self.midi_learning = False
        self.midi_learning_widget = None

        mapstr = self.settings.get_string('midi-map')
        map = json.loads(mapstr)
        self.restore_midi_map(map, builder)

    def on_midi(self, event, channel, value):
        if event == 176 or event == 144:
            # CC and NoteOn
            key = (event, channel)
            print(event, channel, value)

            if key in self.midi_map:
                self.midi_map[key].new_value(value)

            if self.midi_learning:
                self.midi_learning = False
                self.midi_map[key] = MidiLink(self.midi_learning_widget, key, self.midi)
                self.midi_learning_widget = None
                self.save_midi_map()

    def on_context_menu(self, widget, event, *args):
        if event.button != 3:
            return

        self.menu = Gtk.Menu()
        midi_item = Gtk.MenuItem("Midi learn")
        midi_item.connect('activate', self.on_start_midi_learn)
        self.menu.append(midi_item)
        self.menu.show_all()
        self.menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
        self.midi_learning_widget = widget

    def on_start_midi_learn(self, *args):
        self.midi_learning = True

    def restore_midi_map(self, map, builder):
        for line in map:
            *key, widget_name = line
            key = tuple(*key)
            if 'dyn:' in widget_name:
                pass
            else:
                widget = builder.get_object(widget_name)
            link = MidiLink(widget, key, self.midi)
            self.midi_map[key] = link

    def save_midi_map(self):
        result = []
        for key in self.midi_map:
            result.append([key, self.midi_map[key].name])

        js = json.dumps(result)
        self.settings.set_string('midi-map', js)

M gtk_switcher/switcher.py => gtk_switcher/switcher.py +38 -0
@@ 45,9 45,11 @@ class SwitcherPage:
        self.usks = {}
        self.dsks = {}
        self.has_models = []
        self.menu = None

        self.upstream_keyers = builder.get_object('upstream_keyers')
        self.downstream_keyers = builder.get_object('downstream_keyers')
        self.macro_flow = builder.get_object('macro_flow')

        self.usk1_dve_fill = builder.get_object('usk1_dve_fill')
        self.usk1_mask_en = builder.get_object('usk1_mask_en')


@@ 858,3 860,39 @@ class SwitcherPage:
    def on_aux_me_source_changed(self, widget, aux, source):
        cmd = AuxSourceCommand(aux, source=source)
        self.connection.mixer.send_commands([cmd])

    def on_macro_properties_change(self, data):
        # Clear the macro flow container
        for widget in self.macro_flow:
            self.macro_flow.remove(widget)

        # Create new buttons
        macros = self.connection.mixer.mixerstate['macro-properties']
        for index in macros:
            macro = macros[index]
            if macro.is_used:
                button = Gtk.Button(macro.name.decode())
                button.index = index
                button.get_style_context().add_class('bmdbtn')
                button.get_style_context().add_class('macro')
                button.connect('button-press-event', self.on_macro_context)
                self.macro_flow.add(button)
        self.macro_flow.show_all()

    def on_macro_context(self, widget, event, *args):
        if event.button != 3:
            return

        self.menu = Gtk.Menu()
        run_item = Gtk.MenuItem("Run macro")
        self.menu.append(run_item)
        edit_item = Gtk.MenuItem("Edit macro")
        edit_item.index = widget.index
        edit_item.connect('activate', self.on_macro_edit)
        self.menu.append(edit_item)
        self.menu.show_all()
        self.menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())

    def on_macro_edit(self, widget):
        self.macro_edit = True
        self.connection.mixer.download(0xffff, widget.index)

M gtk_switcher/ui/mixer.glade => gtk_switcher/ui/mixer.glade +72 -0
@@ 1421,6 1421,78 @@
                                                <property name="position">4</property>
                                              </packing>
                                            </child>
                                            <child>
                                              <object class="GtkExpander" id="expander_macros">
                                                <property name="visible">True</property>
                                                <property name="can-focus">True</property>
                                                <property name="expanded">True</property>
                                                <child>
                                                  <object class="GtkFrame">
                                                    <property name="visible">True</property>
                                                    <property name="can-focus">False</property>
                                                    <property name="margin-bottom">64</property>
                                                    <property name="label-xalign">0</property>
                                                    <property name="shadow-type">in</property>
                                                    <child>
                                                      <object class="GtkBox">
                                                        <property name="visible">True</property>
                                                        <property name="can-focus">False</property>
                                                        <property name="margin-start">12</property>
                                                        <property name="margin-end">12</property>
                                                        <property name="margin-top">12</property>
                                                        <property name="margin-bottom">12</property>
                                                        <property name="orientation">vertical</property>
                                                        <child>
                                                          <placeholder/>
                                                        </child>
                                                        <child>
                                                          <object class="GtkFlowBox" id="macro_flow">
                                                            <property name="visible">True</property>
                                                            <property name="can-focus">False</property>
                                                            <property name="homogeneous">True</property>
                                                            <property name="column-spacing">4</property>
                                                            <property name="row-spacing">4</property>
                                                          </object>
                                                          <packing>
                                                            <property name="expand">False</property>
                                                            <property name="fill">True</property>
                                                            <property name="position">1</property>
                                                          </packing>
                                                        </child>
                                                        <child>
                                                          <placeholder/>
                                                        </child>
                                                      </object>
                                                    </child>
                                                    <child type="label_item">
                                                      <placeholder/>
                                                    </child>
                                                    <style>
                                                      <class name="view"/>
                                                    </style>
                                                  </object>
                                                </child>
                                                <child type="label">
                                                  <object class="GtkLabel">
                                                    <property name="visible">True</property>
                                                    <property name="can-focus">False</property>
                                                    <property name="margin-bottom">5</property>
                                                    <property name="label" translatable="yes">Macros</property>
                                                    <style>
                                                      <class name="heading"/>
                                                    </style>
                                                  </object>
                                                </child>
                                                <style>
                                                  <class name="bmdgroup"/>
                                                </style>
                                              </object>
                                              <packing>
                                                <property name="expand">False</property>
                                                <property name="fill">True</property>
                                                <property name="position">5</property>
                                              </packing>
                                            </child>
                                          </object>
                                        </child>
                                      </object>