Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Change QMenu to QComboBox #23

Merged
merged 4 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 30 additions & 59 deletions midi_app_controller/gui/binds_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
QPushButton,
QLabel,
QHBoxLayout,
QMenu,
QRadioButton,
QDialog,
QScrollArea,
)

from midi_app_controller.gui.utils import SearchableQComboBox

Check warning on line 16 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L16

Added line #L16 was not covered by tests
from midi_app_controller.models.binds import ButtonBind, KnobBind, Binds
from midi_app_controller.models.controller import Controller, ControllerElement

Expand All @@ -26,9 +26,8 @@
actions : List[str]
List of all actions available to bind and an empty string (used when
no action is bound).
button_menus : Tuple[int, QPushButton]
List of all pairs (button id, QPushButton used to set action). Each
QPushButton text is the currently selected action.
button_menus : Tuple[int, SearchableQComboBox]
List of all pairs (button id, SearchableQComboBox used to set action).
binds_dict : dict[int, ControllerElement]
Dictionary that allows to get a controller's button by its id.
"""
Expand All @@ -53,7 +52,7 @@
super().__init__()

self.actions = [""] + actions
self.button_menus = []
self.button_combos = []

Check warning on line 55 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L55

Added line #L55 was not covered by tests
self.binds_dict = {b.button_id: b for b in button_binds}

# Description row.
Expand All @@ -64,8 +63,8 @@
# All buttons available to bind.
button_list = QWidget()
button_layout = QVBoxLayout()
for elem in buttons:
button_layout.addLayout(self._create_button_layout(elem.id, elem.name))
for button in buttons:
button_layout.addLayout(self._create_button_layout(button.id, button.name))

Check warning on line 67 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L66-L67

Added lines #L66 - L67 were not covered by tests
button_layout.addStretch()
button_list.setLayout(button_layout)

Expand All @@ -85,42 +84,29 @@
"""Creates layout for a button.

The layout consists of button name and action selector. An entry is
added to the `self.button_menus`.
added to the `self.button_combos`.
"""
# Check if there is an action bound to the button.
if (bind := self.binds_dict.get(button_id)) is not None:
action = bind.action_id
else:
action = None

# QPushButton with menu.
button_action = QPushButton(action)
button_action.setMenu(self._create_action_menu(button_action))

self.button_menus.append((button_id, button_action))
# SearchableQComboBox for action selection.
action_combo = SearchableQComboBox(self.actions, action, self)
self.button_combos.append((button_id, action_combo))

Check warning on line 97 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L96-L97

Added lines #L96 - L97 were not covered by tests

layout = QHBoxLayout()
layout.addWidget(QLabel(button_name))
layout.addWidget(button_action)
layout.addWidget(action_combo)

Check warning on line 101 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L101

Added line #L101 was not covered by tests

return layout

def _create_action_menu(self, button: QPushButton) -> QMenu:
"""Creates a scrollable menu consisting of all `self.actions`.

When an action is selected, the text of `button` is set its name.
"""
menu = QMenu(self)
menu.setStyleSheet("QMenu { menu-scrollable: 1; }")
for action in self.actions:
menu.addAction(action, lambda action=action: button.setText(action))
return menu

def get_binds(self) -> List[ButtonBind]:
"""Returns list of all binds currently set in this widget."""
result = []
for button_id, button in self.button_menus:
action = button.text() or None
for button_id, combo in self.button_combos:
action = combo.currentText() or None

Check warning on line 109 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L108-L109

Added lines #L108 - L109 were not covered by tests
if action is not None:
result.append(ButtonBind(button_id=button_id, action_id=action))
return result
Expand All @@ -134,10 +120,9 @@
actions : List[str]
List of all actions available to bind and an empty string (used when
no action is bound).
knob_menus : Tuple[int, QPushButton, QPushButton]
List of all triples (knob id, QPushButton used to set increase action,
QPushButton used to set decrease action). Each QPushButton text is
the currently selected action.
knob_combos : Tuple[int, SearchableQComboBox, SearchableQComboBox]
List of all triples (knob id, SearchableQComboBox used to set increase action,
SearchableQComboBox used to set decrease action).
binds_dict : dict[int, ControllerElement]
Dictionary that allows to get a controller's knob by its id.
"""
Expand All @@ -162,7 +147,7 @@
super().__init__()

self.actions = [""] + actions
self.knob_menus = []
self.knob_combos = []

Check warning on line 150 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L150

Added line #L150 was not covered by tests
self.binds_dict = {b.knob_id: b for b in knob_binds}

# Description row.
Expand Down Expand Up @@ -191,53 +176,39 @@

self.setLayout(layout)

def _create_action_menu(self, knob: QPushButton) -> QMenu:
"""Creates a scrollable menu consisting of all `self.actions`.

When an action is selected, the text of `knob` is set its name.
"""
menu = QMenu(self)
menu.setStyleSheet("QMenu { menu-scrollable: 1; }")
for action in self.actions:
menu.addAction(action, lambda action=action: knob.setText(action))
return menu

def _create_knob_layout(self, knob_id: int, knob_name: str) -> QHBoxLayout:
"""Creates layout for a knob.

The layout consists of knob name and increase/decrease action selector.
An entry is added to the `self.knob_menus`.
An entry is added to the `self.knob_combos`.
"""
# Check if there are any actions bound to the knob.
if (bind := self.binds_dict.get(knob_id)) is not None:
action_increase = bind.action_id_increase
action_decrease = bind.action_id_decrease
else:
action_increase = ""
action_decrease = ""

# QPushButton with menus.
knob_increase = QPushButton(action_increase)
knob_increase.setMenu(self._create_action_menu(knob_increase))
knob_decrease = QPushButton(action_decrease)
knob_decrease.setMenu(self._create_action_menu(knob_decrease))
action_increase = None
action_decrease = None

Check warning on line 191 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L190-L191

Added lines #L190 - L191 were not covered by tests

self.knob_menus.append((knob_id, knob_increase, knob_decrease))
# SearchableQComboBox for action selection.
increase_action_combo = SearchableQComboBox(self.actions, action_increase, self)
decrease_action_combo = SearchableQComboBox(self.actions, action_decrease, self)
self.knob_combos.append((knob_id, increase_action_combo, decrease_action_combo))

Check warning on line 196 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L194-L196

Added lines #L194 - L196 were not covered by tests

# Layout.
layout = QHBoxLayout()
layout.addWidget(QLabel(knob_name))
layout.addWidget(knob_increase)
layout.addWidget(knob_decrease)
layout.addWidget(increase_action_combo)
layout.addWidget(decrease_action_combo)

Check warning on line 202 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L201-L202

Added lines #L201 - L202 were not covered by tests

return layout

def get_binds(self) -> List[KnobBind]:
"""Returns list of all binds currently set in this widget."""
result = []
for knob_id, knob_increase, knob_decrease in self.knob_menus:
increase_action = knob_increase.text() or None
decrease_action = knob_decrease.text() or None
for knob_id, increase_action_combo, decrease_action_combo in self.knob_combos:
increase_action = increase_action_combo.currentText() or None
decrease_action = decrease_action_combo.currentText() or None

Check warning on line 211 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L209-L211

Added lines #L209 - L211 were not covered by tests
if increase_action is not None or decrease_action is not None:
result.append(
KnobBind(
Expand Down Expand Up @@ -333,7 +304,7 @@
self.setLayout(layout)
self.setStyleSheet(get_current_stylesheet())
self.knobs_radio.setChecked(True)
self.setMinimumSize(500, 550)
self.setMinimumSize(830, 650)

Check warning on line 307 in midi_app_controller/gui/binds_editor.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/binds_editor.py#L307

Added line #L307 was not covered by tests

def _switch_editors(self, checked):
"""Switches binds editor view for knobs/buttons based on checked radio."""
Expand Down
120 changes: 33 additions & 87 deletions midi_app_controller/gui/midi_status.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from typing import Callable, List
from typing import List

Check warning on line 2 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L2

Added line #L2 was not covered by tests

from napari._app_model import get_app
from napari._app_model.actions._help_actions import HELP_ACTIONS
Expand All @@ -10,14 +10,14 @@
QWidget,
QVBoxLayout,
QPushButton,
QMenu,
QLabel,
QHBoxLayout,
)

from midi_app_controller.models.binds import ButtonBind, KnobBind, Binds
from midi_app_controller.models.controller import Controller
from midi_app_controller.gui.binds_editor import BindsEditor
from midi_app_controller.gui.utils import DynamicQComboBox

Check warning on line 20 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L20

Added line #L20 was not covered by tests
from midi_app_controller.state.state_manager import StateManager

# TODO I didn't find any better way to get all available actions.
Expand All @@ -30,16 +30,16 @@

Attributes
----------
current_binds : QPushButton
current_binds : DynamicQComboBox
Button that allows to select binds using its menu. Its text
is set to currently selected binds.
current_controller : QPushButton
current_controller : DynamicQComboBox
Button that allows to select controller using its menu. Its text
is set to currently selected controller.
current_midi_in : QPushButton
current_midi_in : DynamicQComboBox
Button that allows to select MIDI input port using its menu. Its
text is set to currently selected port.
current_midi_in : QPushButton
current_midi_in : DynamicQComboBox
Button that allows to select MIDI output port using its menu. Its
text is set to currently selected port.
status : QLabel
Expand All @@ -56,53 +56,36 @@

# Binds selection.
selected_binds = state_manager.selected_binds
self.current_binds = QPushButton(
selected_binds.name if selected_binds is not None else None
)
self.current_binds.setMenu(
self._create_dynamic_menu(
self.current_binds,
state_manager.get_available_binds,
state_manager.select_binds,
)
self.current_binds = DynamicQComboBox(

Check warning on line 59 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L59

Added line #L59 was not covered by tests
selected_binds.name if selected_binds is not None else None,
state_manager.get_available_binds,
state_manager.select_binds,
)

# Controller selection.
selected_controller = state_manager.selected_controller
self.current_controller = QPushButton(
selected_controller.name if selected_controller is not None else None
)

def select_controller(name: str) -> None:
state_manager.select_controller(name)
state_manager.selected_binds = None
self.current_binds.setText(None)

self.current_controller.setMenu(
self._create_dynamic_menu(
self.current_controller,
state_manager.get_available_controllers,
select_controller,
)
self.current_binds.setCurrentText(None)

Check warning on line 69 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L69

Added line #L69 was not covered by tests

selected_controller = state_manager.selected_controller
self.current_controller = DynamicQComboBox(

Check warning on line 72 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L71-L72

Added lines #L71 - L72 were not covered by tests
selected_controller.name if selected_controller is not None else None,
state_manager.get_available_controllers,
select_controller,
)

# MIDI input and output selection.
self.current_midi_in = QPushButton(state_manager.selected_midi_in)
self.current_midi_in.setMenu(
self._create_dynamic_menu(
self.current_midi_in,
state_manager.get_available_midi_in,
state_manager.select_midi_in,
)
self.current_midi_in = DynamicQComboBox(

Check warning on line 79 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L79

Added line #L79 was not covered by tests
state_manager.selected_midi_in,
state_manager.get_available_midi_in,
state_manager.select_midi_in,
)

self.current_midi_out = QPushButton(state_manager.selected_midi_out)
self.current_midi_out.setMenu(
self._create_dynamic_menu(
self.current_midi_out,
state_manager.get_available_midi_out,
state_manager.select_midi_out,
)
self.current_midi_out = DynamicQComboBox(

Check warning on line 85 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L85

Added line #L85 was not covered by tests
state_manager.selected_midi_out,
state_manager.get_available_midi_out,
state_manager.select_midi_out,
)

# Status.
Expand Down Expand Up @@ -134,15 +117,11 @@
# Layout.
layout = QVBoxLayout()
layout.addLayout(
self._create_label_button_layout("Controller:", self.current_controller)
)
layout.addLayout(self._create_label_button_layout("Binds:", self.current_binds))
layout.addLayout(
self._create_label_button_layout("MIDI input:", self.current_midi_in)
)
layout.addLayout(
self._create_label_button_layout("MIDI output:", self.current_midi_out)
self._horizontal_layout("Controller:", self.current_controller)
)
layout.addLayout(self._horizontal_layout("Binds:", self.current_binds))
layout.addLayout(self._horizontal_layout("MIDI input:", self.current_midi_in))
layout.addLayout(self._horizontal_layout("MIDI output:", self.current_midi_out))

Check warning on line 124 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L122-L124

Added lines #L122 - L124 were not covered by tests
layout.addLayout(status_layout)
layout.addWidget(self.edit_binds_button)
layout.addWidget(self.start_handling_button)
Expand All @@ -151,45 +130,12 @@

self.setLayout(layout)

def _create_dynamic_menu(
self,
button: QPushButton,
get_entries: Callable[[], List[str]],
select_entry: Callable[[str], None],
) -> QMenu:
"""Creates a scrollable menu that will display entries from `get_entries()`
each time it's opened.

When an entry is selected:
- the text of `button` is set to the entry,
- `select_entry` is invoked with the entry as argument.
"""
menu = QMenu(self)
menu.setStyleSheet("QMenu { menu-scrollable: 1; }")

def add_actions():
"""Clears the menu and adds entries from `get_entries()`."""
menu.clear()
for elem in get_entries():

def select(elem=elem):
"""Update button's text and run `select_entry()`."""
button.setText(elem)
select_entry(elem)

menu.addAction(elem, select)

menu.aboutToShow.connect(add_actions)
return menu

def _create_label_button_layout(
self, label: str, button: QPushButton
) -> QHBoxLayout:
"""Creates horizontal layout consisting of label on the left half and
button on the right half."""
def _horizontal_layout(self, label: str, widget: QWidget) -> QHBoxLayout:

Check warning on line 133 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L133

Added line #L133 was not covered by tests
"""Creates horizontal layout consisting of the `label` on the left half\
and the `widget` on the right half."""
layout = QHBoxLayout()
layout.addWidget(QLabel(label))
layout.addWidget(button)
layout.addWidget(widget)

Check warning on line 138 in midi_app_controller/gui/midi_status.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/gui/midi_status.py#L138

Added line #L138 was not covered by tests
return layout

def _edit_binds(self):
Expand Down
Loading
Loading