Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

PR: Add new combobox widgets (API) #21555

Merged
merged 15 commits into from
Dec 5, 2023
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
250 changes: 250 additions & 0 deletions spyder/api/widgets/comboboxes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Spyder combobox widgets.

Use these widgets for any combobox you want to add to Spyder.
"""

# Standard library imports
import sys

# Third-party imports
import qstylizer.style
from qtpy.QtCore import QSize, Qt, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QComboBox,
QFontComboBox,
QFrame,
QLineEdit,
QStyledItemDelegate
)

# Local imports
from spyder.utils.palette import QStylePalette
from spyder.utils.stylesheet import AppStyle, WIN


class _SpyderComboBoxDelegate(QStyledItemDelegate):
"""
Delegate to make separators color follow our theme.

Adapted from https://stackoverflow.com/a/33464045/438386
"""

def paint(self, painter, option, index):
data = index.data(Qt.AccessibleDescriptionRole)
if data and data == "separator":
painter.setPen(QColor(QStylePalette.COLOR_BACKGROUND_6))
painter.drawLine(
option.rect.left() + AppStyle.MarginSize,
option.rect.center().y(),
option.rect.right() - AppStyle.MarginSize,
option.rect.center().y()
)
else:
super().paint(painter, option, index)

def sizeHint(self, option, index):
data = index.data(Qt.AccessibleDescriptionRole)
if data and data == "separator":
return QSize(0, 3 * AppStyle.MarginSize)

return super().sizeHint(option, index)


class _SpyderComboBoxLineEdit(QLineEdit):
"""Dummy lineedit used for non-editable comboboxes."""

sig_mouse_clicked = Signal()

def __init__(self, parent):
super().__init__(parent)

# Fix style issues
css = qstylizer.style.StyleSheet()
css.QLineEdit.setValues(
# These are necessary on Windows to prevent some ugly visual
# glitches.
backgroundColor="transparent",
border="none",
padding="0px",
# Make text look centered for short comboboxes
paddingRight=f"-{3 if WIN else 2}px"
)

self.setStyleSheet(css.toString())

def mouseReleaseEvent(self, event):
self.sig_mouse_clicked.emit()
super().mouseReleaseEvent(event)

def mouseDoubleClickEvent(self, event):
# Avoid selecting the lineedit text with double clicks
pass


class _SpyderComboBoxMixin:
"""Mixin with the basic style and functionality for our comboboxes."""

def __init__(self):

# Style
self._css = self._generate_stylesheet()
self.setStyleSheet(self._css.toString())

def contextMenuEvent(self, event):
# Prevent showing context menu for editable comboboxes because it's
# added automatically by Qt. That means that the menu is not built
# using our API and it's not localized.
pass
dalthviz marked this conversation as resolved.
Show resolved Hide resolved

def _generate_stylesheet(self):
"""Base stylesheet for Spyder comboboxes."""
css = qstylizer.style.StyleSheet()

# Make our comboboxes have a uniform height
css.QComboBox.setValues(
minHeight=f'{AppStyle.ComboBoxMinHeight}em'
)

# Add top and bottom padding to the inner contents of comboboxes
css["QComboBox QAbstractItemView"].setValues(
paddingTop=f"{AppStyle.MarginSize + 1}px",
paddingBottom=f"{AppStyle.MarginSize + 1}px"
)

# Add margin and padding to combobox items
css["QComboBox QAbstractItemView::item"].setValues(
marginLeft=f"{AppStyle.MarginSize}px",
marginRight=f"{AppStyle.MarginSize}px",
padding=f"{AppStyle.MarginSize}px"
)

# Make color of hovered combobox items match the one used in other
# Spyder widgets
css["QComboBox QAbstractItemView::item:selected:active"].setValues(
backgroundColor=QStylePalette.COLOR_BACKGROUND_3,
)

return css


class SpyderComboBox(QComboBox, _SpyderComboBoxMixin):
"""Combobox widget for Spyder when its items don't have icons."""

def __init__(self, parent=None):
super().__init__(parent)

self.is_editable = None
self._is_shown = False
self._is_popup_shown = False

# This is also necessary to have more fine-grained control over the
# style of our comboboxes with css, e.g. to add more padding between
# its items.
# See https://stackoverflow.com/a/33464045/438386 for the details.
self.setItemDelegate(_SpyderComboBoxDelegate(self))

def showEvent(self, event):
"""Adjustments when the widget is shown."""

if not self._is_shown:
if not self.isEditable():
self.is_editable = False
self.setLineEdit(_SpyderComboBoxLineEdit(self))

# This is necessary to make Qt position the popup widget below
# the combobox for non-editable ones.
# Solution from https://stackoverflow.com/a/45191141/438386
self.setEditable(True)
self.lineEdit().setReadOnly(True)

# Show popup when the lineEdit is clicked, which is the default
# behavior for non-editable comboboxes in Qt.
self.lineEdit().sig_mouse_clicked.connect(self.showPopup)
else:
self.is_editable = True

self._is_shown = True

super().showEvent(event)

def showPopup(self):
"""Adjustments when the popup is shown."""
super().showPopup()

if sys.platform == "darwin":
# Reposition popup to display it in the right place.
# Solution from https://forum.qt.io/post/349517
popup = self.findChild(QFrame)
popup.move(popup.x() - 3, popup.y() + 4)

# Adjust width to match the lineEdit one.
if not self._is_popup_shown:
popup.setFixedWidth(popup.width() + 2)
self._is_popup_shown = True
else:
# Make borders straight to make popup feel as part of the combobox.
# This doesn't work reliably on Mac.
self._css.QComboBox.setValues(
borderBottomLeftRadius="0px",
borderBottomRightRadius="0px",
)

self.setStyleSheet(self._css.toString())

def hidePopup(self):
"""Adjustments when the popup is hidden."""
super().hidePopup()

if not sys.platform == "darwin":
# Make borders rounded when popup is not visible. This doesn't work
# reliably on Mac.
self._css.QComboBox.setValues(
borderBottomLeftRadius=QStylePalette.SIZE_BORDER_RADIUS,
borderBottomRightRadius=QStylePalette.SIZE_BORDER_RADIUS,
)

self.setStyleSheet(self._css.toString())


class SpyderComboBoxWithIcons(SpyderComboBox):
""""Combobox widget for Spyder when its items have icons."""

def __init__(self, parent=None):
super().__init__(parent)

# Padding is not necessary because icons give items enough of it.
self._css["QComboBox QAbstractItemView::item"].setValues(
padding="0px"
)

self.setStyleSheet(self._css.toString())


class SpyderFontComboBox(QFontComboBox, _SpyderComboBoxMixin):

def __init__(self, parent=None):
super().__init__(parent)

# This is necessary for items to get the style set in our stylesheet.
self.setItemDelegate(QStyledItemDelegate(self))

# Adjust popup width to contents.
self.setSizeAdjustPolicy(
QComboBox.AdjustToMinimumContentsLengthWithIcon
)

def showPopup(self):
"""Adjustments when the popup is shown."""
super().showPopup()

if sys.platform == "darwin":
popup = self.findChild(QFrame)
popup.move(popup.x() - 3, popup.y() + 4)
64 changes: 37 additions & 27 deletions spyder/plugins/appearance/confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
from spyder.widgets.simplecodeeditor import SimpleCodeEditor


PREVIEW_TEXT = (
'"""A string"""\n\n'
'# A comment\n\n'
'class Foo(object):\n'
' def __init__(self):\n'
' bar = 42\n'
' print(bar)\n'
)


class AppearanceConfigPage(PluginConfigPage):

def __init__(self, plugin, parent):
Expand Down Expand Up @@ -79,9 +89,16 @@ def setup_page(self):

self.preview_editor = SimpleCodeEditor(self)
self.preview_editor.setMinimumWidth(210)
self.preview_editor.set_language('Python')
self.preview_editor.set_text(PREVIEW_TEXT)
self.preview_editor.set_blanks_enabled(False)
self.preview_editor.set_scrollpastend_enabled(False)

self.stacked_widget = QStackedWidget(self)
self.scheme_editor_dialog = SchemeEditor(parent=self,
stack=self.stacked_widget)
self.scheme_editor_dialog = SchemeEditor(
parent=self,
stack=self.stacked_widget
)

self.scheme_choices_dict = {}
schemes_combobox_widget = self.create_combobox('', [('', '')],
Expand Down Expand Up @@ -178,10 +195,19 @@ def setup_page(self):
edit_button.clicked.connect(self.edit_scheme)
self.reset_button.clicked.connect(self.reset_to_default)
self.delete_button.clicked.connect(self.delete_scheme)
self.schemes_combobox.currentIndexChanged.connect(self.update_preview)
self.schemes_combobox.currentIndexChanged.connect(
lambda index: self.update_preview()
)
self.schemes_combobox.currentIndexChanged.connect(self.update_buttons)
self.plain_text_font.fontbox.currentFontChanged.connect(
lambda font: self.update_preview()
)
self.plain_text_font.sizebox.valueChanged.connect(
lambda value: self.update_preview()
)
system_font_checkbox.checkbox.stateChanged.connect(
self.update_app_font_group)
self.update_app_font_group
)

# Setup
for name in names:
Expand Down Expand Up @@ -317,34 +343,18 @@ def update_buttons(self):
self.delete_button.setEnabled(delete_enabled)
self.reset_button.setEnabled(not delete_enabled)

def update_preview(self, index=None, scheme_name=None):
"""
Update the color scheme of the preview editor and adds text.

Note
----
'index' is needed, because this is triggered by a signal that sends
the selected index.
"""
text = ('"""A string"""\n\n'
'# A comment\n\n'
'class Foo(object):\n'
' def __init__(self):\n'
' bar = 42\n'
' print(bar)\n'
)

def update_preview(self, scheme_name=None):
"""Update the color scheme of the preview editor and adds text."""
if scheme_name is None:
scheme_name = self.current_scheme

plain_text_font = self.plain_text_font.fontbox.currentFont()
plain_text_font.setPointSize(self.plain_text_font.sizebox.value())

self.preview_editor.setup_editor(
font=get_font(),
color_scheme=scheme_name,
show_blanks=False,
scroll_past_end=False,
font=plain_text_font,
color_scheme=scheme_name
)
self.preview_editor.set_language('Python')
self.preview_editor.set_text(text)

def update_app_font_group(self, state):
"""Update app font group enabled state."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
from qtpy.QtCore import (Qt, Slot, QAbstractTableModel, QModelIndex,
QSize)
from qtpy.QtWidgets import (QAbstractItemView, QCheckBox,
QComboBox, QDialog, QDialogButtonBox, QGroupBox,
QDialog, QDialogButtonBox, QGroupBox,
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QSpinBox, QTableView, QVBoxLayout)

# Local imports
from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType
from spyder.api.widgets.comboboxes import SpyderComboBox
from spyder.config.base import _
from spyder.plugins.completion.api import SUPPORTED_LANGUAGES
from spyder.utils.misc import check_connection_port
Expand Down Expand Up @@ -150,7 +151,7 @@ def __init__(self, parent, language=None, cmd='', host='127.0.0.1',

# Widgets
self.server_settings_description = QLabel(description)
self.lang_cb = QComboBox(self)
self.lang_cb = SpyderComboBox(self)
self.external_cb = QCheckBox(_('External server'), self)
self.host_label = QLabel(_('Host:'))
self.host_input = QLineEdit(self)
Expand Down
5 changes: 3 additions & 2 deletions spyder/plugins/completion/providers/snippets/conftabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
# Third party imports
from qtpy.compat import getsavefilename, getopenfilename
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (QComboBox, QGroupBox, QGridLayout, QLabel,
from qtpy.QtWidgets import (QGroupBox, QGridLayout, QLabel,
QMessageBox, QPushButton, QVBoxLayout, QFileDialog)

# Local imports
from spyder.api.widgets.comboboxes import SpyderComboBox
from spyder.config.base import _
from spyder.config.snippets import SNIPPETS
from spyder.plugins.completion.providers.snippets.widgets import (
Expand Down Expand Up @@ -47,7 +48,7 @@ def __init__(self, parent):
snippets_info_label.setWordWrap(True)
snippets_info_label.setAlignment(Qt.AlignJustify)

self.snippets_language_cb = QComboBox(self)
self.snippets_language_cb = SpyderComboBox(self)
self.snippets_language_cb.setToolTip(
_('Programming language provided by the LSP server'))
self.snippets_language_cb.addItems(SUPPORTED_LANGUAGES_PY)
Expand Down
Loading