diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6692f87..d4116085 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,8 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Setup Audio + run: sudo apt-get install -y pulseaudio - name: Setup QtLibs uses: # See https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions tlambert03/setup-qt-libs@v1 diff --git a/zxlive/base_panel.py b/zxlive/base_panel.py index b0da58bc..0f6d39d0 100644 --- a/zxlive/base_panel.py +++ b/zxlive/base_panel.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import Iterator, Optional, Sequence, Type +from PySide6.QtCore import Signal from PySide6.QtGui import QAction from PySide6.QtWidgets import (QAbstractButton, QButtonGroup, QSplitter, QToolBar, QVBoxLayout, QWidget) @@ -44,6 +45,8 @@ class BasePanel(QWidget): file_path: Optional[str] file_type: Optional[FileFormat] + play_sound_signal = Signal(object) # Actual type: SFXEnum + def __init__(self, *actions: QAction) -> None: super().__init__() self.addActions(actions) diff --git a/zxlive/edit_panel.py b/zxlive/edit_panel.py index 9f281fd2..4fb02289 100644 --- a/zxlive/edit_panel.py +++ b/zxlive/edit_panel.py @@ -48,7 +48,6 @@ def __init__(self, graph: GraphT, *actions: QAction) -> None: self.create_side_bar() self.splitter.addWidget(self.sidebar) - def _toolbar_sections(self) -> Iterator[ToolbarSection]: yield from super()._toolbar_sections() @@ -62,7 +61,6 @@ def _toolbar_sections(self) -> Iterator[ToolbarSection]: self.start_derivation.clicked.connect(self._start_derivation) yield ToolbarSection(self.start_derivation) - def _start_derivation(self) -> None: if not self.graph_scene.g.is_well_formed(): show_error_msg("Graph is not well-formed", parent=self) diff --git a/zxlive/editor_base_panel.py b/zxlive/editor_base_panel.py index ff7ffe6f..6d1c2182 100644 --- a/zxlive/editor_base_panel.py +++ b/zxlive/editor_base_panel.py @@ -14,6 +14,7 @@ from pyzx import EdgeType, VertexType from pyzx.utils import get_w_partner, vertex_is_w from pyzx.graph.jsonparser import string_to_phase +from zxlive.sfx import SFXEnum from .base_panel import BasePanel, ToolbarSection from .commands import (AddEdge, AddNode, AddWNode, ChangeEdgeColor, @@ -146,6 +147,7 @@ def delete_selection(self) -> None: def add_vert(self, x: float, y: float) -> None: cmd = AddWNode(self.graph_view, x, y) if self._curr_vty == VertexType.W_OUTPUT \ else AddNode(self.graph_view, x, y, self._curr_vty) + self.play_sound_signal.emit(SFXEnum.THATS_A_SPIDER) self.undo_stack.push(cmd) def add_edge(self, u: VT, v: VT) -> None: diff --git a/zxlive/mainwindow.py b/zxlive/mainwindow.py index b8f0b794..de7b5af2 100644 --- a/zxlive/mainwindow.py +++ b/zxlive/mainwindow.py @@ -16,11 +16,13 @@ from __future__ import annotations import copy +import random from typing import Callable, Optional, cast -from PySide6.QtCore import (QByteArray, QEvent, QFile, QFileInfo, QIODevice, - QSettings, QTextStream, Qt) -from PySide6.QtGui import QAction, QCloseEvent, QIcon, QKeySequence +from PySide6.QtCore import (QByteArray, QDir, QEvent, QFile, QFileInfo, + QIODevice, QSettings, QTextStream, Qt, QUrl) +from PySide6.QtGui import QAction, QCloseEvent, QIcon, QKeySequence, QShortcut +from PySide6.QtMultimedia import QSoundEffect from PySide6.QtWidgets import (QDialog, QMainWindow, QMessageBox, QTableWidget, QTableWidgetItem, QTabWidget, QVBoxLayout, QWidget) @@ -43,6 +45,7 @@ from .edit_panel import GraphEditPanel from .proof_panel import ProofPanel from .rule_panel import RulePanel +from .sfx import SFXEnum, load_sfx from .tikz import proof_to_tikz @@ -172,6 +175,11 @@ def __init__(self) -> None: menu.setStyleSheet("QMenu::item:disabled { color: gray }") self._reset_menus(False) + self.effects = {e: load_sfx(e) for e in SFXEnum} + + self.sfx_on = self.settings.value("sound-effects") + QShortcut(QKeySequence("Ctrl+B"), self).activated.connect(self._toggle_sfx) + def open_demo_graph(self) -> None: graph = construct_circuit() self.new_graph(graph) @@ -376,6 +384,8 @@ def handle_save_file_action(self) -> bool: out << data file.close() self.active_panel.undo_stack.setClean() + if random.random() < 0.1: + self.play_sound(SFXEnum.IRANIAN_BUS) return True @@ -458,6 +468,7 @@ def _new_panel(self, panel: BasePanel, name: str) -> None: panel.undo_stack.cleanChanged.connect(self.update_tab_name) panel.undo_stack.canUndoChanged.connect(self._undo_changed) panel.undo_stack.canRedoChanged.connect(self._redo_changed) + panel.play_sound_signal.connect(self.play_sound) def new_graph(self, graph: Optional[GraphT] = None, name: Optional[str] = None) -> None: _graph = graph or new_graph() @@ -577,6 +588,18 @@ def update_colors(self) -> None: if self.active_panel is not None: self.active_panel.update_colors() + def play_sound(self, s: SFXEnum) -> None: + if self.sfx_on: + self.effects[s].play() + + def _toggle_sfx(self) -> None: + self.sfx_on = not self.sfx_on + if self.sfx_on: + self.play_sound(random.choice([ + SFXEnum.WELCOME_EVERYBODY, + SFXEnum.OK_IM_GONNA_START, + ])) + def update_font(self) -> None: self.menuBar().setFont(display_setting.font) for i in range(self.tab_widget.count()): diff --git a/zxlive/proof_panel.py b/zxlive/proof_panel.py index 13b73ea0..662b296c 100644 --- a/zxlive/proof_panel.py +++ b/zxlive/proof_panel.py @@ -26,6 +26,7 @@ from .rewrite_action import RewriteActionTreeModel from .rewrite_data import action_groups, refresh_custom_rules from .settings import display_setting +from .sfx import SFXEnum from .vitem import SCALE, W_INPUT_OFFSET, DragState, VItem @@ -159,11 +160,13 @@ def _vertex_dropped_onto(self, v: VT, w: VT) -> None: pyzx.basicrules.fuse(g, w, v) anim = anims.fuse(self.graph_scene.vertex_map[v], self.graph_scene.vertex_map[w]) cmd = AddRewriteStep(self.graph_view, g, self.step_view, "fuse spiders") + self.play_sound_signal.emit(SFXEnum.THATS_SPIDER_FUSION) self.undo_stack.push(cmd, anim_before=anim) elif pyzx.basicrules.check_strong_comp(g, v, w): pyzx.basicrules.strong_comp(g, w, v) anim = anims.strong_comp(self.graph, g, w, self.graph_scene) cmd = AddRewriteStep(self.graph_view, g, self.step_view, "bialgebra") + self.play_sound_signal.emit(SFXEnum.BOOM_BOOM_BOOM) self.undo_stack.push(cmd, anim_after=anim) def _wand_trace_finished(self, trace: WandTrace) -> None: diff --git a/zxlive/settings.py b/zxlive/settings.py index 80a65221..07ffa988 100644 --- a/zxlive/settings.py +++ b/zxlive/settings.py @@ -35,6 +35,7 @@ class ColorScheme(TypedDict): "snap-granularity": '4', "input-circuit-format": 'openqasm', "previews-show": 'True' + 'sound-effects': False, } font_defaults: dict[str, str | int | None] = { diff --git a/zxlive/settings_dialog.py b/zxlive/settings_dialog.py index 755835ee..257b66c5 100644 --- a/zxlive/settings_dialog.py +++ b/zxlive/settings_dialog.py @@ -25,7 +25,7 @@ from PySide6.QtWidgets import ( QDialog, QFileDialog, QFormLayout, QLineEdit, QPushButton, QWidget, QVBoxLayout, QSpinBox, QDoubleSpinBox, QLabel, QHBoxLayout, QTabWidget, - QComboBox, QApplication + QComboBox, QApplication, QCheckBox ) from .common import get_settings_value, T, get_data @@ -43,6 +43,7 @@ class FormInputType(IntEnum): Float = 2 Folder = 3 Combo = 4 + Bool = 5 class SettingsData(TypedDict): @@ -83,7 +84,8 @@ class SettingsData(TypedDict): {"id": "tab-bar-location", "label": "Tab bar location", "type": FormInputType.Combo, "data": tab_positioning_data}, {"id": "snap-granularity", "label": "Snap-to-grid granularity", "type": FormInputType.Combo, "data": snap_to_grid_data}, {"id": "input-circuit-format", "label": "Input Circuit as", "type": FormInputType.Combo, "data": input_circuit_formats}, - {"id": "previews-show", "label": "Show rewrite previews","type": FormInputType.Combo, "data": combo_true_false} + {"id": "previews-show", "label": "Show rewrite previews","type": FormInputType.Combo, "data": combo_true_false}, + {"id": "sound-effects", "label": "Sound Effects", "type": FormInputType.Bool}, ] @@ -201,6 +203,11 @@ def init_okay_cancel_buttons(self, layout: QVBoxLayout) -> None: cancel_button.clicked.connect(self.cancel) hlayout.addWidget(cancel_button) + def make_bool_form_input(self, data: SettingsData) -> QCheckBox: + widget = QCheckBox() + widget.setChecked(self.get_settings_from_data(data, bool)) + return widget + def make_str_form_input(self, data: SettingsData) -> QLineEdit: widget = QLineEdit() widget.setText(self.get_settings_from_data(data, str)) @@ -253,6 +260,7 @@ def add_setting_to_form(self, form: QFormLayout, settings_data: SettingsData) -> FormInputType.Float: self.make_float_form_input, FormInputType.Folder: self.make_folder_form_input, FormInputType.Combo: self.make_combo_form_input, + FormInputType.Bool: self.make_bool_form_input, } setting_widget_maker = setting_maker[settings_data["type"]] widget: QWidget = setting_widget_maker(settings_data) diff --git a/zxlive/sfx.py b/zxlive/sfx.py new file mode 100644 index 00000000..aa84591e --- /dev/null +++ b/zxlive/sfx.py @@ -0,0 +1,22 @@ +from enum import Enum +from PySide6.QtCore import QDir, QUrl +from PySide6.QtMultimedia import QSoundEffect + +class SFXEnum(Enum): + BOOM_BOOM_BOOM = "boom-boom-boom.wav" + IRANIAN_BUS = "iranian-bus.wav" + OK_IM_GONNA_START = "ok-im-gonna-start.wav" + THATS_A_SPIDER = "thats-a-spider.wav" + THATS_SPIDER_FUSION = "thats-spider-fusion.wav" + THEY_FALL_OFF = "they-fall-off.wav" + WELCOME_EVERYBODY = "welcome-everybody.wav" + + +def load_sfx(e: SFXEnum) -> QSoundEffect: + """Load a sound effect from a file.""" + effect = QSoundEffect() + fullpath = QDir.current().absoluteFilePath("zxlive/sfx/" + e.value) + url = QUrl.fromLocalFile(fullpath) + effect.setSource(url) + + return effect diff --git a/zxlive/sfx/boom-boom-boom.wav b/zxlive/sfx/boom-boom-boom.wav new file mode 100644 index 00000000..5c707772 Binary files /dev/null and b/zxlive/sfx/boom-boom-boom.wav differ diff --git a/zxlive/sfx/iranian-bus.wav b/zxlive/sfx/iranian-bus.wav new file mode 100644 index 00000000..59bc490e Binary files /dev/null and b/zxlive/sfx/iranian-bus.wav differ diff --git a/zxlive/sfx/ok-im-gonna-start.wav b/zxlive/sfx/ok-im-gonna-start.wav new file mode 100644 index 00000000..86c89bac Binary files /dev/null and b/zxlive/sfx/ok-im-gonna-start.wav differ diff --git a/zxlive/sfx/thats-a-spider.wav b/zxlive/sfx/thats-a-spider.wav new file mode 100644 index 00000000..3bfb3b35 Binary files /dev/null and b/zxlive/sfx/thats-a-spider.wav differ diff --git a/zxlive/sfx/thats-spider-fusion.wav b/zxlive/sfx/thats-spider-fusion.wav new file mode 100644 index 00000000..4dd1f489 Binary files /dev/null and b/zxlive/sfx/thats-spider-fusion.wav differ diff --git a/zxlive/sfx/they-fall-off.wav b/zxlive/sfx/they-fall-off.wav new file mode 100644 index 00000000..a368db49 Binary files /dev/null and b/zxlive/sfx/they-fall-off.wav differ diff --git a/zxlive/sfx/welcome-everybody.wav b/zxlive/sfx/welcome-everybody.wav new file mode 100644 index 00000000..79cdef39 Binary files /dev/null and b/zxlive/sfx/welcome-everybody.wav differ