Hello everyone.
So for my CZI importer, there are two things I think need to be user-selectable at import time:
- A CZI file may contain multiple scenes, and a user may decide which scenes need importing.
- The channel colours in the CZI file may not be optimal, so being able to change them during the import phase may also be desirable.
My question is, would it be possible to make this selection accessible from the import panel? Maybe via “Show options”?
Or, do I show a custom selection panel as an intermediate step?
Clicking on the “…” button (or pressing Enter) brings up the color picker.
Hidden spaghetti code below
For now, this is just a small experiment conducted in the Jupyter extension, hence the splits corresponding to my various cells. If you find any of this code useful, feel free to re-use it any way you like, ChatGPT certainly did .
import slicer
import qt
from qt import Qt
Here’s the code to select colors in a drop down menu:
class CustomComboBox(qt.QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
def focusInEvent(self, event):
# Pass the focus to the parent ComboChanCol
if self.parent():
self.parent().setFocus()
class ComboChanCol(qt.QWidget):
"""
Custom Qt Widget to show a dropdown menu with the channels
and a button to change the color.
If no colors are provided, then they are automatically set to a HSV sequence
that hopefully makes sense.
"""
def __init__(self, channels, colors=None, *args, **kwargs):
super(ComboChanCol, self).__init__(*args, **kwargs)
# Create the horizontal layout and combobox
layout = qt.QHBoxLayout()
#layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self._comboBox = CustomComboBox() #qt.QComboBox()
layout.addWidget(self._comboBox)
# Create the button with three dots
self._button = qt.QPushButton("...")
self._button.setFixedHeight(self._comboBox.sizeHint.height())
self._button.setMaximumWidth(30) # Adjust the width as needed
layout.addWidget(self._button)
self.setLayout(layout)
print(channels)
print(colors)
if colors is None:
n = len(channels)
self._colors = self.generate_colors(n)
else:
self._colors = [c if type(c) == qt.QColor else qt.QColor(c) for c in colors]
for c,n in zip(self._colors,channels):
icon = self.create_solid_color_icon(c)
self._comboBox.addItem(icon,n)
#Connect the button
self._button.connect('clicked()', self.on_pick_color)
def keyPressEvent(self, event):
# Check if the Enter key is pressed and if this widget has focus
if event.key() == Qt.Key_Return and self.hasFocus():
self.on_pick_color()
elif event.key() == qt.Qt.Key_Up or event.key() == qt.Qt.Key_Down:
# Pass the up and down arrow key events to the QComboBox
self._comboBox.event(event)
def on_pick_color(self):
channel_index = self._comboBox.currentIndex
channel_name = self._comboBox.currentText
initial_color = self._colors[channel_index]
color_dialog = qt.QColorDialog(self)
color_dialog.setCurrentColor(initial_color)
color_dialog.setWindowTitle('Select Color for '+channel_name)
# Show the dialog and check the result
result = color_dialog.exec_()
if result == qt.QColorDialog.Accepted:
icon = self.create_solid_color_icon(color_dialog.currentColor)
self._comboBox.setItemIcon(channel_index, icon)
self._colors[channel_index] = color_dialog.currentColor
def generate_colors(self, n):
colors = []
for i in range(n):
hue = i * (360 / n) # Divide the hue range evenly
saturation = 255 # Maximum saturation
value = 255 # Maximum value (brightness)
# Create a QColor with the specified HSV values
color = qt.QColor.fromHsv(hue, saturation, value)
colors.append(color)
return colors
def create_solid_color_icon(self, color, size=8):
pixmap = qt.QPixmap(size,size)
pixmap.fill(color)
return qt.QIcon(pixmap)
def get_colors(self,as_hex=False):
return [c.name() if as_hex else c for c in self._colors]
Here’s the code for selecting scenes via checkboxes in a drop down menu:
class CheckboxComboBox(qt.QComboBox):
def __init__(self, items, states=None):
super().__init__()
self.setView(qt.QListView()) # Set the view for the drop-down list
self.activated.connect(self.activated_custom) # Connect the activated signal to your custom slot
#check if states is the same size as items
if states is not None and len(items) != len(states):
raise ValueError("states and items must have the same length")
self.item_dict = {}
self.setModel(qt.QStandardItemModel())
for i, item in enumerate(items):
checkbox_item = qt.QStandardItem(item)
checkbox_item.setCheckable(True)
if states is not None:
checkbox_item.setCheckState(
qt.Qt.Checked if states[i]
else qt.Qt.Unchecked
)
self.item_dict[item] = checkbox_item
self.model().appendRow(checkbox_item)
self.setView(qt.QListView()) # Set the view for the drop-down list
def activated_custom(self, index):
selected_item = self.model().item(index) #.row())
if selected_item:
selected_text = selected_item.text() # Get the selected item text
if selected_text in self.item_dict:
checkbox_item = self.item_dict[selected_text]
print(selected_text, checkbox_item.checkState())
checkbox_item.setCheckState(
qt.Qt.Checked if checkbox_item.checkState() == qt.Qt.Unchecked
else qt.Qt.Unchecked
)
def get_checked_items(self):
checked_items = {}
for item, checkbox_item in self.item_dict.items():
checked_items[item] = checkbox_item.checkState() == qt.Qt.Checked
return checked_items
And here’s a dialog box that shows both with a text window for padding:
class TestWindow(qt.QWidget):
def __init__(self, parent=None, scenes=[], channels=[], colors=None):
super(TestWindow, self).__init__(parent)
parent.resize(400,400)
parent.setWindowTitle("CZI Scene importer")
# Create a QDialogButtonBox with OK and Cancel buttons
buttonBox = qt.QDialogButtonBox()
okButton = buttonBox.addButton(qt.QDialogButtonBox.Ok)
cancelButton = buttonBox.addButton(qt.QDialogButtonBox.Cancel)
layout = qt.QFormLayout()
states = [True] * len(scenes)
self.combo_bc = CheckboxComboBox(scenes, states)
self._scenes_dict = self.combo_bc.get_checked_items()
layout.addRow("Scenes :", self.combo_bc)
self.ccc = ComboChanCol(channels, colors)
self._colors = self.ccc.get_colors()
layout.addRow("Colors :", self.ccc)
big_editor = qt.QTextEdit()
layout.addWidget(big_editor)
layout.addWidget(buttonBox)
parent.setLayout(layout)
buttonBox.accepted.connect(self.ok_callback)
buttonBox.rejected.connect(self.cancel_callback)
def ok_callback(self):
self._colors = self.ccc.get_colors()
self._scenes_dict = self.combo_bc.get_checked_items()
self.parentWidget().accept()
def cancel_callback(self):
self.parentWidget().reject()
def get_colors(self, as_hex=False):
return [c.name() if as_hex else c for c in self._colors]
def get_scenes(self):
return self._scenes_dict
scenes = ["Scene %d"%i for i in range(10)]
channels = ["Channel %d"%i for i in range(5)]
colors = None
messagePopup = qt.QDialog()
tw = TestWindow(messagePopup,scenes,channels,colors)
print("Before:", tw.get_colors(as_hex=True))
result = messagePopup.exec_()
print("After:", result, tw.get_colors(as_hex=True))
print(tw.get_scenes())
The user experience (UX) with these widgets is pretty poor in my opinion, so that’s why I’m asking for potential ideas to streamline the process. Of course, if the user is happy with importing all the scenes and the colours are fine, then it’s just a matter of clicking on OK (or Cancel).
Cheers,
Egor