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
62 changes: 57 additions & 5 deletions src/superqt/cmap/_cmap_combo.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from cmap import Colormap
from qtpy.QtCore import QSortFilterProxyModel, QStringListModel, Qt, Signal
from qtpy.QtCore import (
QEvent,
QObject,
QSortFilterProxyModel,
QStringListModel,
Qt,
Signal,
)
from qtpy.QtWidgets import (
QButtonGroup,
QCheckBox,
QComboBox,
QCompleter,
QDialog,
QDialogButtonBox,
QMenu,
QSizePolicy,
QVBoxLayout,
QWidget,
Expand All @@ -27,7 +35,7 @@
from collections.abc import Sequence

from cmap._colormap import ColorStopsLike
from qtpy.QtGui import QKeyEvent
from qtpy.QtGui import QKeyEvent, QMouseEvent


CMAP_ROLE = Qt.ItemDataRole.UserRole + 1
Expand Down Expand Up @@ -109,6 +117,10 @@ def __init__(
self.currentIndexChanged.connect(self._on_index_changed)
line_edit.editingFinished.connect(self._on_editing_finished)

# Enable right-click "Remove" on dropdown items
if (view := self.view()) and (viewport := view.viewport()):
viewport.installEventFilter(self)

def userAdditionsAllowed(self) -> bool:
"""Returns whether the user can add custom colors."""
return self._allow_user_colors
Expand Down Expand Up @@ -204,7 +216,20 @@ def setCurrentColormap(self, color: Any) -> None:

for idx in range(self.count()):
if (item := self.itemColormap(idx)) and item.name == cmap.name:
# cmap_ is already here - just select it
self.setCurrentIndex(idx)
return

# cmap not in the combo box yet — add it, then select it
self.addColormap(cmap)
idx = self.findData(cmap, CMAP_ROLE)
if idx >= 0:
old_idx = self.currentIndex()
self.setCurrentIndex(idx)
if idx == old_idx:
# setCurrentIndex won't emit if the index didn't actually change
# (e.g. first item added at index 0 when current index is already 0)
self._on_index_changed(idx)

def _on_activated(self, index: int) -> None:
if self.itemText(index) != self._add_color_text:
Expand All @@ -217,8 +242,7 @@ def _on_activated(self, index: int) -> None:
if (item := self.itemColormap(i)) and cmap.name == item.name:
self.setCurrentIndex(i)
return
self.addColormap(cmap)
self.currentIndexChanged.emit(self.currentIndex())
self.setCurrentColormap(cmap)
elif self._last_cmap is not None:
# user canceled, restore previous color without emitting signal
idx = self.findData(self._last_cmap, CMAP_ROLE)
Expand Down Expand Up @@ -257,6 +281,34 @@ def _on_editing_finished(self) -> None:
if self.findData(cmap, CMAP_ROLE) < 0:
self.addColormap(cmap)

def eventFilter(self, obj: QObject | None, event: QEvent | None) -> bool:
if event and event.type() == QEvent.Type.MouseButtonRelease:
mouse_event = cast("QMouseEvent", event)
if mouse_event.button() == Qt.MouseButton.RightButton:
view = self.view()
if view and obj is view.viewport():
index = view.indexAt(mouse_event.pos())
if index.isValid():
text = self.itemText(index.row())
if text != self._add_color_text:
self._show_remove_menu(view, mouse_event.pos(), index.row())
return True # consume the right-click
return super().eventFilter(obj, event)

def _show_remove_menu(self, view: Any, pos: Any, row: int) -> None:
menu = QMenu(view)
remove_action = menu.addAction("Remove")
if menu.exec(view.viewport().mapToGlobal(pos)) == remove_action:
was_current = row == self.currentIndex()
self.removeItem(row)
self._update_completer_model()
if was_current:
# select the first actual colormap, skipping "Add Colormap..."
for i in range(self.count()):
if self.itemColormap(i) is not None:
self.setCurrentIndex(i)
return

def keyPressEvent(self, e: QKeyEvent | None) -> None:
if e and e.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
# select the first completion when pressing enter if the popup is visible
Expand Down
44 changes: 28 additions & 16 deletions src/superqt/cmap/_cmap_line_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

from qtpy.QtCore import QRect, Qt
from qtpy.QtGui import QIcon, QPainter, QPaintEvent, QPalette
from qtpy.QtWidgets import QApplication, QLineEdit, QStyle, QWidget
from qtpy.QtWidgets import (
QApplication,
QLineEdit,
QStyle,
QStyleOptionFrame,
QWidget,
)

from ._cmap_utils import draw_colormap, pick_font_color, try_cast_colormap

Expand Down Expand Up @@ -56,7 +62,7 @@ def __init__(
self,
parent: QWidget | None = None,
*,
fractional_colormap_width: float = 0.33,
fractional_colormap_width: float = 0.35,
fallback_cmap: Colormap | str | None = "gray",
missing_icon: QIcon | QStyle.StandardPixmap = MISSING,
checkerboard_size: int = 4,
Expand Down Expand Up @@ -150,27 +156,35 @@ def setColormap(self, cmap: Colormap | str | None) -> None:
def _cmap_is_full_width(self):
return self._colormap_fraction >= 0.75

def _content_rect(self) -> QRect:
"""Return the style-aware content rect for this line edit."""
opt = QStyleOptionFrame()
self.initStyleOption(opt)
return self.style().subElementRect(
QStyle.SubElement.SE_LineEditContents, opt, self
)

def _cmap_rect(self) -> QRect:
cmap_rect = self.rect().adjusted(2, 0, 0, 0)
cmap_rect.setWidth(int(cmap_rect.width() * self._colormap_fraction))
return cmap_rect
cr = self._content_rect()
# Apply the horizontal content inset as vertical padding too,
# so the swatch sits inside the style's painted background.
v_pad = cr.x() - self.rect().x()
return QRect(
max(cr.x(), 2),
v_pad,
int(cr.width() * self._colormap_fraction),
self.rect().height() - 2 * v_pad,
)

def resizeEvent(self, e: Any) -> None:
left_margin = 6
if not self._cmap_is_full_width():
# leave room for the colormap
left_margin += self._cmap_rect().width()
left_margin = self._cmap_rect().right() + 4 - self.rect().x()
self.setTextMargins(left_margin, 2, 0, 0)
super().resizeEvent(e)

def paintEvent(self, e: QPaintEvent) -> None:
# don't draw the background
# otherwise it will cover the colormap during super().paintEvent
# FIXME: this appears to need to be reset during every paint event...
# otherwise something is resetting it
palette = self.palette()
palette.setColor(palette.ColorRole.Base, Qt.GlobalColor.transparent)
self.setPalette(palette)
super().paintEvent(e) # let the style paint background + text first

cmap_rect = self._cmap_rect()
if self._cmap:
Expand All @@ -181,5 +195,3 @@ def paintEvent(self, e: QPaintEvent) -> None:
if self._missing_cmap:
draw_colormap(self, self._missing_cmap, cmap_rect)
self._missing_icon.paint(QPainter(self), cmap_rect.adjusted(4, 4, 0, -4))

super().paintEvent(e) # draw text (must come after draw_colormap)
Loading