Skip to content
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pickmix"
version = "0.4.0"
version = "0.4.1"
description = "Multi-kind particle picking GUI for cryo-ET sub-tomogram averaging workflows."
readme = "README.md"
authors = [
Expand Down
9 changes: 8 additions & 1 deletion src/pickmix/gui/components/range_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Import external dependencies
from typing import Literal
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
from PySide6.QtCore import QEvent, QPointF, QRectF, QSize, Qt, Signal
from PySide6.QtGui import QKeyEvent, QMouseEvent, QPainter, QPaintEvent, QPen
from PySide6.QtWidgets import QSizePolicy, QWidget

Expand Down Expand Up @@ -50,6 +50,13 @@ def set_values(self, low: int, high: int, *, emit: bool = False) -> None:
if changed and emit:
self.valueChanged.emit(low, high)

# event: claim up and down keys before a window shortcut can
def event(self, event: QEvent) -> bool:
if event.type() == QEvent.Type.ShortcutOverride and isinstance(event, QKeyEvent) and event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down):
event.accept()
return True
return super().event(event)

# keyPressEvent: Left and Right move the active handle, Up and Down switch handle, PageUp and PageDown move by a tenth
def keyPressEvent(self, event: QKeyEvent) -> None:
small, large = max(1, (self._max - self._min) // 100), max(1, (self._max - self._min) // 10)
Expand Down
9 changes: 8 additions & 1 deletion src/pickmix/gui/tabs/pick_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import numpy as np
from concurrent.futures import Future
from PySide6.QtCore import Qt
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
QButtonGroup, QCheckBox, QFormLayout, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QHeaderView, QLabel, QPushButton,
QScrollBar, QSlider, QSplitter, QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget,
Expand Down Expand Up @@ -51,7 +52,7 @@ def __init__(self) -> None:
self._range = (0.0, 1.0)
self.canvas = SliceCanvas()
self.title = title()
self.help = hint('Enter: next tomogram. Shift+Enter: previous tomogram.')
self.help = hint('Enter: next tomogram. Shift+Enter: previous tomogram. Up/Down: change Z slice; Shift+Up/Down: change Z slice by 10.')
self.status = QLabel()
self.match_pane = MatchPane()
self.tomo = TomoPane()
Expand Down Expand Up @@ -184,6 +185,8 @@ def __init__(self) -> None:
self.canvas.move_requested.connect(self._move)
self.canvas.marker_clicked.connect(self._clicked)
self.canvas.z_scrolled.connect(lambda steps: self.z_slider.setValue(self.z_slider.value() + steps))
for keys, step in (('Up', 1), ('Down', -1), ('Shift+Up', 10), ('Shift+Down', -10)):
QShortcut(QKeySequence(keys), self, context=Qt.ShortcutContext.WidgetWithChildrenShortcut).activated.connect(lambda step=step: self._step_z(step))
self.table.cellClicked.connect(self._row_clicked)
self.match_pane.scan_requested.connect(self._scan)
self.match_pane.stop_requested.connect(self._stop_scan)
Expand Down Expand Up @@ -396,6 +399,10 @@ def _open_done(self, name: str, future: Future[Opened]) -> None:
if self.session.index + 1 < len(self.session.names):
self.loader.prefetch(self.session.names[self.session.index + 1])

# _step_z: move the slice by steps
def _step_z(self, steps: int) -> None:
self.z_slider.setValue(self.z_slider.value() + steps)

# _show_z: show slice z, from the cache or after reading it in the background
def _show_z(self, z: int) -> None:
if self.opened is None or self.loader is None:
Expand Down
3 changes: 2 additions & 1 deletion tests/gui/test_main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def test_pick_keys_are_window_wide(self, qtbot: QtBot) -> None:
qtbot.addWidget(window)
keys = {s.key().toString(): s for s in window.findChildren(QShortcut)}
assert all(str(i) in keys for i in range(1, 10)) and 'Del' in keys and 'Backspace' in keys and 'A' in keys
assert all(s.context() == Qt.ShortcutContext.WindowShortcut for s in keys.values())
wanted = [*(str(i) for i in range(1, 10)), 'Del', 'Backspace', 'A']
assert all(keys[k].context() == Qt.ShortcutContext.WindowShortcut for k in wanted)

# test_hotkey_and_delete_from_other_focus: with focus off the canvas, a hotkey still picks the kind and delete still removes the pick
def test_hotkey_and_delete_from_other_focus(self, qtbot: QtBot, make_config: Callable[..., Path]) -> None:
Expand Down
45 changes: 45 additions & 0 deletions tests/gui/test_pick_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,51 @@ def test_scroll_z(self, setup: Callable[..., tuple[PickTab, Session]]) -> None:
tab.canvas.z_scrolled.emit(-9)
assert tab.z == 0

# test_arrow_keys_scroll_z: Up and Down move one slice and Shift ten, clamped to the tomogram
def test_arrow_keys_scroll_z(self, qtbot: QtBot, setup: Callable[..., tuple[PickTab, Session]]) -> None:
tab, _ = setup()
tab.show()
qtbot.waitExposed(tab)
tab.activateWindow()
qtbot.waitUntil(tab.isActiveWindow, timeout=3000)
tab.canvas.setFocus()
qtbot.keyClick(tab.canvas, Qt.Key.Key_Down)
assert tab.z == 1
qtbot.keyClick(tab.canvas, Qt.Key.Key_Up)
qtbot.keyClick(tab.canvas, Qt.Key.Key_Up)
assert tab.z == 3
qtbot.keyClick(tab.canvas, Qt.Key.Key_Up)
assert tab.z == 3
qtbot.keyClick(tab.canvas, Qt.Key.Key_Down, Qt.KeyboardModifier.ShiftModifier)
assert tab.z == 0

# test_arrow_keys_from_table: arrows scroll z even when the pick table has focus
def test_arrow_keys_from_table(self, qtbot: QtBot, setup: Callable[..., tuple[PickTab, Session]]) -> None:
tab, session = setup()
tab.pick_kind(0)
tab._add(1.0, 1.0)
tab.show()
qtbot.waitExposed(tab)
tab.activateWindow()
qtbot.waitUntil(tab.isActiveWindow, timeout=3000)
tab.table.setFocus()
before = tab.z
qtbot.keyClick(tab.table, Qt.Key.Key_Up)
assert tab.z == before + 1
assert session.store.total() == 1

# test_arrow_keys_leave_contrast_slider: Up and Down still switch the active handle of the contrast slider
def test_arrow_keys_leave_contrast_slider(self, qtbot: QtBot, setup: Callable[..., tuple[PickTab, Session]]) -> None:
tab, _ = setup()
tab.show()
qtbot.waitExposed(tab)
tab.activateWindow()
qtbot.waitUntil(tab.isActiveWindow, timeout=3000)
tab.contrast.setFocus()
before = tab.z
qtbot.keyClick(tab.contrast, Qt.Key.Key_Up)
assert tab.z == before

# test_delete_needs_selection: Delete is greyed until a pick is selected and again once it is gone
def test_delete_needs_selection(self, setup: Callable[..., tuple[PickTab, Session]]) -> None:
tab, _ = setup()
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading