kitbash 1.4.0__tar.gz → 1.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kitbash-1.4.0 → kitbash-1.5.1}/PKG-INFO +3 -3
- kitbash-1.5.1/kitbash/__init__.py +127 -0
- kitbash-1.4.0/kitbash/__init__.py → kitbash-1.5.1/kitbash/__main__.py +6 -43
- kitbash-1.5.1/kitbash/gui/__init__.py +21 -0
- kitbash-1.5.1/kitbash/gui/drumkit_widget.py +438 -0
- kitbash-1.5.1/kitbash/gui/kit_save_dialog.py +113 -0
- kitbash-1.5.1/kitbash/gui/main_window.py +660 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/gui/main_window.ui +82 -45
- kitbash-1.5.1/kitbash/jack_audio.py +243 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/scripts/bash_project.py +20 -10
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/styles/light.css +9 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/styles/system.css +76 -0
- kitbash-1.5.1/kitbash/worker_threads.py +72 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/pyproject.toml +3 -3
- {kitbash-1.4.0 → kitbash-1.5.1}/tests/file_save_dialog.py +22 -7
- kitbash-1.4.0/kitbash/gui/__init__.py +0 -92
- kitbash-1.4.0/kitbash/gui/main_window.py +0 -1228
- {kitbash-1.4.0 → kitbash-1.5.1}/.gitignore +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/LICENSE +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/README.md +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/res/empty.sfz +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/res/group_expanded.svg +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/res/group_hidden.svg +0 -0
- {kitbash-1.4.0 → kitbash-1.5.1}/kitbash/res/kitbash-icon.png +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: kitbash
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.5.1
|
|
4
|
+
Summary: Provides universal settings, styles, cached icons, and pixmaps, and the base
|
|
5
5
|
Author-email: Leon Dionne <ldionne@dridesign.sh.cn>
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -9,7 +9,7 @@ Requires-Dist: PyQt5
|
|
|
9
9
|
Requires-Dist: conn_jack >= 1.2.0, <= 1.3.0
|
|
10
10
|
Requires-Dist: liquiphy
|
|
11
11
|
Requires-Dist: soundfile
|
|
12
|
-
Requires-Dist: sfzen >=
|
|
12
|
+
Requires-Dist: sfzen >= 2.2.0
|
|
13
13
|
Requires-Dist: midi_notes >= 1.1.0
|
|
14
14
|
Requires-Dist: recent_items_list >= 1.1.1
|
|
15
15
|
Requires-Dist: soso_qt_extras >= 1.2.0
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# kitbash/kitbash/__init__.py
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025-2026 Leon Dionne <ldionne@dridesign.sh.cn>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program; if not, write to the Free Software
|
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
18
|
+
# MA 02110-1301, USA.
|
|
19
|
+
#
|
|
20
|
+
"""
|
|
21
|
+
Provides universal settings, styles, cached icons, and pixmaps, and the base
|
|
22
|
+
class of windows which save and restore their own geometry.
|
|
23
|
+
"""
|
|
24
|
+
import logging
|
|
25
|
+
from os.path import dirname, basename, splitext, join
|
|
26
|
+
from glob import glob
|
|
27
|
+
from functools import lru_cache
|
|
28
|
+
from PyQt5.QtCore import QSettings
|
|
29
|
+
from PyQt5.QtWidgets import QApplication, QSplitter
|
|
30
|
+
from qt_extras import DevilBox
|
|
31
|
+
from conn_jack import JackConnectError
|
|
32
|
+
|
|
33
|
+
__version__ = "1.5.1"
|
|
34
|
+
|
|
35
|
+
APPLICATION_NAME = "kitbash"
|
|
36
|
+
PACKAGE_DIR = dirname(__file__)
|
|
37
|
+
DEFAULT_STYLE = 'system'
|
|
38
|
+
AUDIO_ICON_SIZE = 16
|
|
39
|
+
KEY_STYLE = 'Style'
|
|
40
|
+
KEY_SAMPLES_MODE = 'KitSaveDialog/SamplesMode'
|
|
41
|
+
KEY_RECENT_DRUMKIT_FOLDER = 'RecentDrumkitFolder'
|
|
42
|
+
KEY_RECENT_DRUMKITS = 'RecentDrumkits'
|
|
43
|
+
KEY_RECENT_PROJECT_FOLDER = 'RecentProjectFolder'
|
|
44
|
+
KEY_RECENT_PROJECTS = 'RecentProjects'
|
|
45
|
+
KEY_SAMPLE_XPLORE_ROOT = 'SampleExplorer/Root'
|
|
46
|
+
KEY_SAMPLE_XPLORE_CURR = 'SampleExplorer/Current'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lru_cache
|
|
50
|
+
def __settings():
|
|
51
|
+
return QSettings('ZenSoSo', APPLICATION_NAME)
|
|
52
|
+
|
|
53
|
+
def get_setting(key, default = None, type_ = None):
|
|
54
|
+
value = __settings().value(key, default)
|
|
55
|
+
if type_:
|
|
56
|
+
if value is None:
|
|
57
|
+
return type_()
|
|
58
|
+
if type_ is bool:
|
|
59
|
+
return value == '1'
|
|
60
|
+
return type_(value)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
def set_setting(key, value):
|
|
64
|
+
if isinstance(value, bool):
|
|
65
|
+
value = '1' if value else '0'
|
|
66
|
+
__settings().setValue(key, value)
|
|
67
|
+
|
|
68
|
+
def delete_setting(key):
|
|
69
|
+
__settings().remove(key)
|
|
70
|
+
|
|
71
|
+
@lru_cache
|
|
72
|
+
def styles():
|
|
73
|
+
return {
|
|
74
|
+
splitext(basename(path))[0] : path \
|
|
75
|
+
for path in glob(join(PACKAGE_DIR, 'styles', '*.css'))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def set_application_style():
|
|
79
|
+
style = get_setting(KEY_STYLE, DEFAULT_STYLE)
|
|
80
|
+
with open(styles()[style], 'r', encoding = 'utf-8') as cssfile:
|
|
81
|
+
QApplication.instance().setStyleSheet(cssfile.read())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GeometrySaver:
|
|
85
|
+
"""
|
|
86
|
+
Provides classes declared in this project which inherit from QDialog methods to
|
|
87
|
+
easily save/restore window / splitter geometry.
|
|
88
|
+
|
|
89
|
+
Geometry is saved in this project's QSettings accessed as "settings()"
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def restore_geometry(self):
|
|
93
|
+
if not hasattr(self, 'restoreGeometry'):
|
|
94
|
+
logging.error('Object of type %s has no "restoreGeometry" function',
|
|
95
|
+
self.__class__.__name__)
|
|
96
|
+
return
|
|
97
|
+
geometry = get_setting(self.__geometry_key())
|
|
98
|
+
if geometry is not None:
|
|
99
|
+
self.restoreGeometry(geometry) # pylint: disable = no-member
|
|
100
|
+
# pylint: disable-next = no-member
|
|
101
|
+
for splitter in self.findChildren(QSplitter):
|
|
102
|
+
geometry = get_setting(self.__splitter_geometry_key(splitter))
|
|
103
|
+
if geometry is not None:
|
|
104
|
+
splitter.restoreState(geometry) # pylint: disable = no-member
|
|
105
|
+
|
|
106
|
+
def save_geometry(self):
|
|
107
|
+
if not hasattr(self, 'saveGeometry'):
|
|
108
|
+
logging.error('Object of type %s has no "saveGeometry" function',
|
|
109
|
+
self.__class__.__name__)
|
|
110
|
+
return
|
|
111
|
+
set_setting(
|
|
112
|
+
self.__geometry_key(),
|
|
113
|
+
self.saveGeometry()) # pylint: disable = no-member
|
|
114
|
+
# pylint: disable-next = no-member
|
|
115
|
+
for splitter in self.findChildren(QSplitter):
|
|
116
|
+
set_setting(
|
|
117
|
+
self.__splitter_geometry_key(splitter),
|
|
118
|
+
splitter.saveState()) # pylint: disable = no-member
|
|
119
|
+
|
|
120
|
+
def __geometry_key(self):
|
|
121
|
+
return f'{self.__class__.__name__}/geometry'
|
|
122
|
+
|
|
123
|
+
def __splitter_geometry_key(self, splitter):
|
|
124
|
+
return f'{self.__class__.__name__}/{splitter.objectName()}/geometry'
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# end kitbash/kitbash/__init__.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# kitbash/__init__.py
|
|
1
|
+
# kitbash/kitbash/__init__.py
|
|
2
2
|
#
|
|
3
|
-
# Copyright
|
|
3
|
+
# Copyright 2025-2026 Leon Dionne <ldionne@dridesign.sh.cn>
|
|
4
4
|
#
|
|
5
5
|
# This program is free software; you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -21,53 +21,16 @@
|
|
|
21
21
|
kitbash is a program you can use to combine parts of various SFZ files into a
|
|
22
22
|
new SFZ with instruments "borrowed" from the originals.
|
|
23
23
|
"""
|
|
24
|
-
import sys, os, argparse, logging
|
|
25
|
-
try:
|
|
26
|
-
from functools import cache
|
|
27
|
-
except ImportError:
|
|
28
|
-
from functools import lru_cache as cache
|
|
29
|
-
from PyQt5.QtCore import QSettings
|
|
24
|
+
import sys, os, argparse, logging
|
|
30
25
|
from PyQt5.QtWidgets import QApplication
|
|
31
26
|
from qt_extras import DevilBox
|
|
32
27
|
from conn_jack import JackConnectError
|
|
28
|
+
from kitbash.gui.main_window import MainWindow
|
|
33
29
|
|
|
34
|
-
__version__ = "1.4.0"
|
|
35
|
-
|
|
36
|
-
APP_NAME = "kitbash"
|
|
37
|
-
APP_PATH = os.path.dirname(__file__)
|
|
38
|
-
DEFAULT_STYLE = 'system'
|
|
39
|
-
KEY_STYLE = 'Style'
|
|
40
|
-
KEY_SAMPLES_MODE = 'KitSaveDialog/SamplesMode'
|
|
41
|
-
KEY_RECENT_DRUMKIT_FOLDER = 'RecentDrumkitFolder'
|
|
42
|
-
KEY_RECENT_DRUMKITS = 'RecentDrumkits'
|
|
43
|
-
KEY_RECENT_PROJECT_FOLDER = 'RecentProjectFolder'
|
|
44
|
-
KEY_RECENT_PROJECTS = 'RecentProjects'
|
|
45
|
-
KEY_SAMPLE_XPLORE_ROOT = 'SampleExplorer/Root'
|
|
46
|
-
KEY_SAMPLE_XPLORE_CURR = 'SampleExplorer/Current'
|
|
47
|
-
|
|
48
|
-
@cache
|
|
49
|
-
def settings():
|
|
50
|
-
return QSettings('ZenSoSo', 'kitbash')
|
|
51
|
-
|
|
52
|
-
@cache
|
|
53
|
-
def styles():
|
|
54
|
-
return {
|
|
55
|
-
os.path.splitext(os.path.basename(path))[0] : path \
|
|
56
|
-
for path in glob.glob(os.path.join(APP_PATH, 'styles', '*.css'))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
def set_application_style():
|
|
60
|
-
style = settings().value(KEY_STYLE, DEFAULT_STYLE)
|
|
61
|
-
with open(styles()[style], 'r', encoding = 'utf-8') as cssfile:
|
|
62
|
-
QApplication.instance().setStyleSheet(cssfile.read())
|
|
63
30
|
|
|
64
31
|
def main():
|
|
65
|
-
from kitbash.gui.main_window import MainWindow
|
|
66
|
-
|
|
67
32
|
p = argparse.ArgumentParser()
|
|
68
|
-
p.epilog =
|
|
69
|
-
Write your help text!
|
|
70
|
-
"""
|
|
33
|
+
p.epilog = __doc__
|
|
71
34
|
p.add_argument('Filename', type=str, nargs='?', help='SFZ file[s] to include at startup')
|
|
72
35
|
p.add_argument("--log-file", "-l", type=str, help="Log to this file")
|
|
73
36
|
p.add_argument("--verbose", "-v", action="store_true", help="Show more detailed debug information")
|
|
@@ -110,4 +73,4 @@ if __name__ == "__main__":
|
|
|
110
73
|
sys.exit(main())
|
|
111
74
|
|
|
112
75
|
|
|
113
|
-
# end kitbash/__init__.py
|
|
76
|
+
# end kitbash/kitbash/__init__.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# kitbash/kitbash/gui/__init__.py
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025-2026 Leon Dionne <ldionne@dridesign.sh.cn>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program; if not, write to the Free Software
|
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
18
|
+
# MA 02110-1301, USA.
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
# end kitbash/kitbash/gui/__init__.py
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# kitbash/kitbash/gui/drumkit_widget.py
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025-2026 Leon Dionne <ldionne@dridesign.sh.cn>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program; if not, write to the Free Software
|
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
18
|
+
# MA 02110-1301, USA.
|
|
19
|
+
#
|
|
20
|
+
"""
|
|
21
|
+
Provides MainWindow.
|
|
22
|
+
"""
|
|
23
|
+
import logging
|
|
24
|
+
from os.path import basename, join
|
|
25
|
+
from functools import partial, lru_cache
|
|
26
|
+
from PyQt5.QtCore import (Qt, QObject, pyqtSignal, pyqtSlot, QSize)
|
|
27
|
+
from PyQt5.QtGui import QIcon
|
|
28
|
+
from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QLabel,
|
|
29
|
+
QFrame, QSizePolicy, QPushButton, QCheckBox)
|
|
30
|
+
from qt_extras import SigBlock
|
|
31
|
+
from sfzen.drumkits import PercussionInstrument
|
|
32
|
+
from kitbash import PACKAGE_DIR
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache
|
|
36
|
+
def group_expanded_icon():
|
|
37
|
+
"""
|
|
38
|
+
Defers loading of QPixmaps until a QGuiApplication is instantiated.
|
|
39
|
+
This is a Qt5 requirement.
|
|
40
|
+
"""
|
|
41
|
+
return QIcon(join(PACKAGE_DIR, 'res', 'group_expanded.svg'))
|
|
42
|
+
|
|
43
|
+
@lru_cache
|
|
44
|
+
def group_hidden_icon():
|
|
45
|
+
return QIcon(join(PACKAGE_DIR, 'res', 'group_hidden.svg'))
|
|
46
|
+
|
|
47
|
+
@lru_cache
|
|
48
|
+
def remove_icon():
|
|
49
|
+
return QIcon.fromTheme('edit-delete')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DrumkitWidget(QFrame):
|
|
53
|
+
"""
|
|
54
|
+
Graphical representation of a Drumkit.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
sig_inst_toggle = pyqtSignal(QObject, str, bool, bool)
|
|
58
|
+
sig_note_on = pyqtSignal(int)
|
|
59
|
+
sig_note_off = pyqtSignal(int)
|
|
60
|
+
sig_remove_drumkit = pyqtSignal(QObject)
|
|
61
|
+
|
|
62
|
+
def __init__(self, filename, parent):
|
|
63
|
+
super().__init__(parent)
|
|
64
|
+
self.sfz_filename = filename
|
|
65
|
+
self.moniker = basename(self.sfz_filename)
|
|
66
|
+
self.drumkit = None
|
|
67
|
+
self.port_number = None
|
|
68
|
+
self.initial_height = None
|
|
69
|
+
|
|
70
|
+
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
|
71
|
+
|
|
72
|
+
main_layout = QVBoxLayout()
|
|
73
|
+
main_layout.setContentsMargins(1,1,1,1)
|
|
74
|
+
main_layout.setSpacing(0)
|
|
75
|
+
|
|
76
|
+
frm_top = QFrame()
|
|
77
|
+
frm_top.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
78
|
+
frm_top.setObjectName('frm_top')
|
|
79
|
+
|
|
80
|
+
lo_top = QHBoxLayout()
|
|
81
|
+
lo_top.setContentsMargins(2,2,2,2)
|
|
82
|
+
lo_top.setSpacing(0)
|
|
83
|
+
|
|
84
|
+
self.hide_button = QPushButton(self)
|
|
85
|
+
self.hide_button.setIcon(group_expanded_icon())
|
|
86
|
+
self.hide_button.setIconSize(QSize(16,16))
|
|
87
|
+
self.hide_button.setCheckable(True)
|
|
88
|
+
self.hide_button.toggled.connect(self.slot_hide)
|
|
89
|
+
lo_top.addWidget(self.hide_button)
|
|
90
|
+
|
|
91
|
+
label = QLabel(self)
|
|
92
|
+
label.setText(self.sfz_filename)
|
|
93
|
+
lo_top.addWidget(label)
|
|
94
|
+
|
|
95
|
+
self.lbl_use_count = QLabel(self)
|
|
96
|
+
self.lbl_use_count.setText('(0)')
|
|
97
|
+
lo_top.addWidget(self.lbl_use_count)
|
|
98
|
+
|
|
99
|
+
lo_top.addStretch(20)
|
|
100
|
+
|
|
101
|
+
remove_button = QPushButton(self)
|
|
102
|
+
remove_button.setIcon(remove_icon())
|
|
103
|
+
remove_button.setIconSize(QSize(16,16))
|
|
104
|
+
remove_button.clicked.connect(self.slot_remove_clicked)
|
|
105
|
+
lo_top.addWidget(remove_button)
|
|
106
|
+
|
|
107
|
+
frm_top.setLayout(lo_top)
|
|
108
|
+
|
|
109
|
+
main_layout.addWidget(frm_top)
|
|
110
|
+
|
|
111
|
+
self.frm_groups = QFrame(self)
|
|
112
|
+
self.frm_groups.setObjectName('frm_groups')
|
|
113
|
+
self.groups = QHBoxLayout()
|
|
114
|
+
self.groups.setContentsMargins(1,1,1,1)
|
|
115
|
+
self.groups.setSpacing(0)
|
|
116
|
+
|
|
117
|
+
self.frm_groups.setLayout(self.groups)
|
|
118
|
+
main_layout.addWidget(self.frm_groups)
|
|
119
|
+
self.setLayout(main_layout)
|
|
120
|
+
|
|
121
|
+
def ready(self):
|
|
122
|
+
"""
|
|
123
|
+
Returns "True" if this DrumkitWidget has a Drumkit assigned.
|
|
124
|
+
"""
|
|
125
|
+
return not self.drumkit is None
|
|
126
|
+
|
|
127
|
+
def set_drumkit(self, drumkit):
|
|
128
|
+
"""
|
|
129
|
+
Called when KitLoader is finshed loading and interpreted Drumkit.
|
|
130
|
+
Fills the groups and instruments of this the DrumkitWidget.
|
|
131
|
+
"""
|
|
132
|
+
self.drumkit = drumkit
|
|
133
|
+
for group in self.drumkit.percussion_groups.values():
|
|
134
|
+
if group.is_empty():
|
|
135
|
+
continue
|
|
136
|
+
group_frame = GroupFrame(group, self)
|
|
137
|
+
group_frame.group_button.clicked.connect(partial(self.slot_group_clicked, group_frame))
|
|
138
|
+
for inst in group.instruments.values():
|
|
139
|
+
inst_button = InstrumentButton(inst, group_frame)
|
|
140
|
+
inst_button.toggled.connect(partial(self.slot_instrument_toggled, inst_button))
|
|
141
|
+
inst_button.sig_mouse_press.connect(self.slot_instrument_pressed)
|
|
142
|
+
inst_button.sig_mouse_release.connect(self.slot_instrument_released)
|
|
143
|
+
group_frame.group_layout.addWidget(inst_button)
|
|
144
|
+
group_frame.group_layout.addStretch()
|
|
145
|
+
self.groups.addWidget(group_frame)
|
|
146
|
+
self.groups.addStretch()
|
|
147
|
+
|
|
148
|
+
@pyqtSlot(QFrame)
|
|
149
|
+
def slot_group_clicked(self, group_frame):
|
|
150
|
+
"""
|
|
151
|
+
Triggered by a GroupButton click event.
|
|
152
|
+
"group_frame" is the QFrame which contains the clicked GroupButton and various
|
|
153
|
+
InstrumentButton instances.
|
|
154
|
+
InstrumentButton signals are not suppressed, and trigger "sig_inst_toggle".
|
|
155
|
+
"""
|
|
156
|
+
group_button = group_frame.findChild(GroupButton)
|
|
157
|
+
for inst_button in group_frame.findChildren(InstrumentButton):
|
|
158
|
+
inst_button.setChecked(group_button.isChecked())
|
|
159
|
+
|
|
160
|
+
@pyqtSlot(PercussionInstrument)
|
|
161
|
+
def slot_instrument_pressed(self, inst):
|
|
162
|
+
"""
|
|
163
|
+
Triggered by InstrumentButton mouse press.
|
|
164
|
+
"""
|
|
165
|
+
self.sig_note_on.emit(inst.pitch)
|
|
166
|
+
|
|
167
|
+
@pyqtSlot(PercussionInstrument)
|
|
168
|
+
def slot_instrument_released(self, inst):
|
|
169
|
+
"""
|
|
170
|
+
Triggered by InstrumentButton mouse relase.
|
|
171
|
+
"""
|
|
172
|
+
self.sig_note_off.emit(inst.pitch)
|
|
173
|
+
|
|
174
|
+
@pyqtSlot(QPushButton)
|
|
175
|
+
def slot_instrument_toggled(self, button):
|
|
176
|
+
"""
|
|
177
|
+
Triggered by an InstrumentButton toggle event.
|
|
178
|
+
"inst_id" is a string key, enumerated in the DrumkitClass.
|
|
179
|
+
"button" is the InstrumentButton which was toggled.
|
|
180
|
+
"""
|
|
181
|
+
self.sig_inst_toggle.emit(self, button.inst.inst_id,
|
|
182
|
+
button.isChecked(), self.ctrl_pressed())
|
|
183
|
+
self.update_count()
|
|
184
|
+
|
|
185
|
+
@pyqtSlot()
|
|
186
|
+
def slot_remove_clicked(self):
|
|
187
|
+
"""
|
|
188
|
+
Triggered by the "remove" button click event.
|
|
189
|
+
"""
|
|
190
|
+
self.sig_remove_drumkit.emit(self)
|
|
191
|
+
|
|
192
|
+
@pyqtSlot(bool)
|
|
193
|
+
def slot_hide(self, state):
|
|
194
|
+
"""
|
|
195
|
+
"Roll up" this DrumkitWidget.
|
|
196
|
+
"""
|
|
197
|
+
if state:
|
|
198
|
+
self.initial_height = self.height()
|
|
199
|
+
self.frm_groups.hide()
|
|
200
|
+
self.hide_button.setIcon(group_hidden_icon())
|
|
201
|
+
else:
|
|
202
|
+
self.frm_groups.show()
|
|
203
|
+
self.hide_button.setIcon(group_expanded_icon())
|
|
204
|
+
|
|
205
|
+
def update_count(self):
|
|
206
|
+
"""
|
|
207
|
+
Updates the "use count" label with the number of selected instruments.
|
|
208
|
+
Sets the audio indicator pixmap based on if playing or not.
|
|
209
|
+
"""
|
|
210
|
+
use_count = len([ b for b in self.frm_groups.findChildren(InstrumentButton) if b.isChecked() ])
|
|
211
|
+
self.lbl_use_count.setText(f'({use_count})')
|
|
212
|
+
font = self.lbl_use_count.font()
|
|
213
|
+
font.setBold(bool(use_count))
|
|
214
|
+
self.lbl_use_count.setFont(font)
|
|
215
|
+
|
|
216
|
+
def ctrl_pressed(self):
|
|
217
|
+
"""
|
|
218
|
+
Returns (bool) True if the CTRL key is being pressed. Useful for making
|
|
219
|
+
multiple selections.
|
|
220
|
+
"""
|
|
221
|
+
return QApplication.keyboardModifiers() == Qt.ControlModifier
|
|
222
|
+
|
|
223
|
+
def inst_button(self, inst_id):
|
|
224
|
+
"""
|
|
225
|
+
Returns the instrument button identified by the given inst_id.
|
|
226
|
+
"""
|
|
227
|
+
return self.findChild(InstrumentButton, inst_id)
|
|
228
|
+
|
|
229
|
+
def deselect_parent_group(self, inst_id):
|
|
230
|
+
"""
|
|
231
|
+
Called whenever an InstrumentButton is deselected.
|
|
232
|
+
The parent group button is deselected.
|
|
233
|
+
"""
|
|
234
|
+
inst_button = self.inst_button(inst_id)
|
|
235
|
+
inst_button.parentWidget().findChild(GroupButton).setChecked(False)
|
|
236
|
+
|
|
237
|
+
def reselect_parent_group(self, inst_id):
|
|
238
|
+
"""
|
|
239
|
+
Called whenever an InstrumentButton is selected.
|
|
240
|
+
The parent group button is selected if all of its' InstrumentButtons are
|
|
241
|
+
selected.
|
|
242
|
+
"""
|
|
243
|
+
group = self.inst_button(inst_id).parentWidget()
|
|
244
|
+
if all(inst_button.isChecked() for inst_button in group.findChildren(InstrumentButton)):
|
|
245
|
+
group.findChild(GroupButton).setChecked(True)
|
|
246
|
+
|
|
247
|
+
def deselect_instrument(self, inst_id):
|
|
248
|
+
"""
|
|
249
|
+
Called from MainWindow when a button with the same inst_id is selected
|
|
250
|
+
exclusively (not CTRL key pressed).
|
|
251
|
+
"""
|
|
252
|
+
button = self.inst_button(inst_id)
|
|
253
|
+
if button: # May not exist, as not all Drumkits use the same instruments
|
|
254
|
+
button.setChecked(False)
|
|
255
|
+
self.update_count()
|
|
256
|
+
|
|
257
|
+
def selected_instruments(self):
|
|
258
|
+
"""
|
|
259
|
+
Returns a list of PercussionInstrument from selected instrument buttons.
|
|
260
|
+
"""
|
|
261
|
+
return [ button.inst \
|
|
262
|
+
for button in self.findChildren(InstrumentButton) \
|
|
263
|
+
if button.isChecked() ]
|
|
264
|
+
|
|
265
|
+
@pyqtSlot()
|
|
266
|
+
def slot_select_all(self):
|
|
267
|
+
"""
|
|
268
|
+
Select all instruments.
|
|
269
|
+
Triggered by kits_area context menu.
|
|
270
|
+
Called when this is the first DrumkitWidget added to a project.
|
|
271
|
+
"""
|
|
272
|
+
for type_ in [GroupButton, InstrumentButton]:
|
|
273
|
+
for button in self.findChildren(type_):
|
|
274
|
+
button.setChecked(True)
|
|
275
|
+
self.update_count()
|
|
276
|
+
|
|
277
|
+
def saved_selections(self):
|
|
278
|
+
"""
|
|
279
|
+
Returns dictionary of button states for saving with project.
|
|
280
|
+
"""
|
|
281
|
+
return {
|
|
282
|
+
group_frame.group_id : {
|
|
283
|
+
'group' : group_frame.findChild(GroupButton).isChecked(),
|
|
284
|
+
'instruments' : {
|
|
285
|
+
inst_button.inst.inst_id : inst_button.isChecked() \
|
|
286
|
+
for inst_button in group_frame.findChildren(InstrumentButton)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for group_frame in self.findChildren(GroupFrame)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def apply_selections(self, selections):
|
|
293
|
+
"""
|
|
294
|
+
Restores button states from dictionary when loading project.
|
|
295
|
+
"""
|
|
296
|
+
for group_frame in self.findChildren(GroupFrame):
|
|
297
|
+
if group_frame.group_id in selections:
|
|
298
|
+
sel = selections[group_frame.group_id]
|
|
299
|
+
group_button = group_frame.findChild(GroupButton)
|
|
300
|
+
with SigBlock(group_button):
|
|
301
|
+
group_button.setChecked(sel['group'])
|
|
302
|
+
for inst_button in group_frame.findChildren(InstrumentButton):
|
|
303
|
+
if inst_button.inst.inst_id in sel['instruments']:
|
|
304
|
+
inst_button.setChecked(sel['instruments'][inst_button.inst.inst_id])
|
|
305
|
+
else:
|
|
306
|
+
logging.warning('Button "%s" not found in project def', inst_button.inst.inst_id)
|
|
307
|
+
else:
|
|
308
|
+
logging.warning('Group "%s" not found in project def', group_frame.group_id)
|
|
309
|
+
|
|
310
|
+
def __str__(self):
|
|
311
|
+
return f"<DrumkitWidget {self.moniker}>"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class GroupFrame(QFrame):
|
|
315
|
+
"""
|
|
316
|
+
QFrame which contains one GroupButton and one or more InstrumentButton
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(self, group, parent):
|
|
320
|
+
super().__init__(parent)
|
|
321
|
+
self.setFrameShape(QFrame.NoFrame)
|
|
322
|
+
self.setObjectName(group.group_id) # GroupFrame identified by group_id
|
|
323
|
+
self.group_id = group.group_id
|
|
324
|
+
self.group_layout = QVBoxLayout()
|
|
325
|
+
self.group_layout.setSpacing(0)
|
|
326
|
+
self.group_layout.setContentsMargins(0,0,0,0)
|
|
327
|
+
self.setLayout(self.group_layout)
|
|
328
|
+
self.group_button = GroupButton(self) # GroupButton has no unique object name
|
|
329
|
+
self.group_button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
330
|
+
self.group_button.setText(group.name)
|
|
331
|
+
self.group_button.setCheckable(True)
|
|
332
|
+
self.group_layout.addWidget(self.group_button)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class GroupButton(QPushButton):
|
|
336
|
+
"""
|
|
337
|
+
Defined here to provide a distinct .css class name.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class InstrumentButton(QPushButton):
|
|
342
|
+
"""
|
|
343
|
+
Custom button with a contained InstrumentLabel and QCheckBox.
|
|
344
|
+
The InstrumentLabel traps mouse press events, while the QCheckBox mirrors the
|
|
345
|
+
"checked" state of this QPushButton.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
sig_mouse_press = pyqtSignal(PercussionInstrument)
|
|
349
|
+
sig_mouse_release = pyqtSignal(PercussionInstrument)
|
|
350
|
+
|
|
351
|
+
def __init__(self, inst, parent):
|
|
352
|
+
super().__init__(parent)
|
|
353
|
+
self.inst = inst
|
|
354
|
+
self.setObjectName(inst.inst_id)
|
|
355
|
+
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
356
|
+
lo = QHBoxLayout()
|
|
357
|
+
lo.setContentsMargins(0,0,0,0)
|
|
358
|
+
lo.setSpacing(0)
|
|
359
|
+
self.setLayout(lo)
|
|
360
|
+
self.setCheckable(True)
|
|
361
|
+
lo.addWidget(InstrumentLabel(inst, self))
|
|
362
|
+
lo.addStretch()
|
|
363
|
+
self.checkbox = QCheckBox(self)
|
|
364
|
+
self.checkbox.stateChanged.connect(self.slot_checkbox_state_change)
|
|
365
|
+
lo.addWidget(self.checkbox)
|
|
366
|
+
|
|
367
|
+
@pyqtSlot(int)
|
|
368
|
+
def slot_checkbox_state_change(self, state):
|
|
369
|
+
"""
|
|
370
|
+
Triggered when contained checkbox is clicked.
|
|
371
|
+
"""
|
|
372
|
+
self.setChecked(state == Qt.Checked)
|
|
373
|
+
|
|
374
|
+
# pylint: disable-next = invalid-name
|
|
375
|
+
def checkStateSet(self):
|
|
376
|
+
"""
|
|
377
|
+
Extends QAbstractButton.checkStateSet.
|
|
378
|
+
This is called in response to the gui setting the "checked" property of this QPushButton.
|
|
379
|
+
"""
|
|
380
|
+
with SigBlock(self.checkbox):
|
|
381
|
+
self.checkbox.setChecked(self.isChecked())
|
|
382
|
+
|
|
383
|
+
# pylint: disable-next = invalid-name
|
|
384
|
+
def mousePressEvent(self, event):
|
|
385
|
+
"""
|
|
386
|
+
Overrides mouse so that only the contained checkbox will toggle this widget's state.
|
|
387
|
+
"""
|
|
388
|
+
event.accept()
|
|
389
|
+
self.mouse_press()
|
|
390
|
+
|
|
391
|
+
# pylint: disable-next = invalid-name
|
|
392
|
+
def mouseReleaseEvent(self, event):
|
|
393
|
+
"""
|
|
394
|
+
Overrides mouse so that only the contained checkbox will toggle this widget's state.
|
|
395
|
+
"""
|
|
396
|
+
event.accept()
|
|
397
|
+
self.mouse_release()
|
|
398
|
+
|
|
399
|
+
def mouse_press(self):
|
|
400
|
+
"""
|
|
401
|
+
Called from contained label. Sets the "down" state of this widget,
|
|
402
|
+
(which is identified in the CSS as the ":pressed" pseudo-selector).
|
|
403
|
+
"""
|
|
404
|
+
self.setDown(True)
|
|
405
|
+
self.sig_mouse_press.emit(self.inst)
|
|
406
|
+
|
|
407
|
+
def mouse_release(self):
|
|
408
|
+
"""
|
|
409
|
+
Called from contained label. Unsets the "down" state of this widget,
|
|
410
|
+
(which is identified in the CSS as the ":pressed" pseudo-selector).
|
|
411
|
+
"""
|
|
412
|
+
self.setDown(False)
|
|
413
|
+
self.sig_mouse_release.emit(self.inst)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class InstrumentLabel(QLabel):
|
|
417
|
+
"""
|
|
418
|
+
Label contained inside an InstrumentButton, delegating its' mouse press /
|
|
419
|
+
release events to its' parent.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
def __init__(self, inst, parent):
|
|
423
|
+
super().__init__(parent)
|
|
424
|
+
self.setText(inst.name)
|
|
425
|
+
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
|
|
426
|
+
|
|
427
|
+
# pylint: disable-next = invalid-name
|
|
428
|
+
def mousePressEvent(self, event):
|
|
429
|
+
self.parent().mouse_press()
|
|
430
|
+
event.accept()
|
|
431
|
+
|
|
432
|
+
# pylint: disable-next = invalid-name
|
|
433
|
+
def mouseReleaseEvent(self, event):
|
|
434
|
+
self.parent().mouse_release()
|
|
435
|
+
event.accept()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# end kitbash/kitbash/gui/drumkit_widget.py
|