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

Add tab completions for PathCombobox #2692

Merged
merged 10 commits into from
Nov 16, 2015
8 changes: 3 additions & 5 deletions spyderlib/plugins/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class ObjectComboBox(EditableComboBox):
QComboBox handling object names
"""
# Signals
valid = Signal(bool)
valid = Signal(bool, bool)

def __init__(self, parent):
EditableComboBox.__init__(self, parent)
Expand Down Expand Up @@ -107,15 +107,13 @@ def validate(self, qstr, editing=True):
if editing:
# Combo box text is being modified: invalidate the entry
self.show_tip(self.tips[valid])
self.valid.emit(False)
self.valid.emit(False, False)
else:
# A new item has just been selected
if valid:
self.selected()
else:
self.valid.emit(False)
else:
self.set_default_style()
self.valid.emit(False, False)


class ObjectInspectorConfigPage(PluginConfigPage):
Expand Down
1 change: 1 addition & 0 deletions spyderlib/plugins/workingdirectory.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def __init__(self, parent, workdir=None, **kwds):
workdir = "."
self.chdir(workdir)
self.pathedit.addItems( wdhistory )
self.pathedit.selected_text = self.pathedit.currentText()
self.refresh_plugin()
self.addWidget(self.pathedit)

Expand Down
221 changes: 153 additions & 68 deletions spyderlib/widgets/comboboxes.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,97 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2009-2010 Pierre Raybaut
# Copyright © 2010-2015 The Spyder Development Team
# Licensed under the terms of the MIT License
# (see spyderlib/__init__.py for details)

"""Customized combobox widgets"""
"""Customized combobox widgets."""

# pylint: disable=C0103
# pylint: disable=R0903
# pylint: disable=R0911
# pylint: disable=R0201

from spyderlib.qt.QtGui import (QComboBox, QFont, QToolTip, QSizePolicy,
QCompleter)
from spyderlib.qt.QtCore import Signal, Qt, QUrl, QTimer

# Standard library imports
import glob
import os
import os.path as osp

# Third party imports
from spyderlib.qt.QtCore import QEvent, Qt, QTimer, QUrl, Signal
from spyderlib.qt.QtGui import (QComboBox, QCompleter, QFont,
QSizePolicy, QToolTip)

# Local imports
from spyderlib.config.base import _
from spyderlib.py3compat import to_text_string
from spyderlib.widgets.helperwidgets import IconLineEdit


class BaseComboBox(QComboBox):
"""Editable combo box base class"""
valid = Signal(bool)

valid = Signal(bool, bool)
sig_tab_pressed = Signal(bool)
sig_double_tab_pressed = Signal(bool)

def __init__(self, parent):
QComboBox.__init__(self, parent)
self.setEditable(True)
self.setCompleter(QCompleter(self))
self.numpress = 0
self.selected_text = self.currentText()

# --- Qt overrides
def event(self, event):
"""Qt Override.

Filter tab keys and process double tab keys.
"""
if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Tab):
self.sig_tab_pressed.emit(True)
self.numpress += 1
if self.numpress == 1:
self.presstimer = QTimer.singleShot(400, self.handle_keypress)
return True
return QComboBox.event(self, event)

# --- overrides
def keyPressEvent(self, event):
"""Handle key press events"""
"""Qt Override.

Handle key press events.
"""
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
if self.add_current_text_if_valid():
self.selected()
self.hide_completer()
elif event.key() == Qt.Key_Escape:
self.set_current_text(self.selected_text)
self.hide_completer()
else:
QComboBox.keyPressEvent(self, event)

def focusOutEvent(self, event):
"""Handle focus out event"""
# Calling asynchronously the 'add_current_text' to avoid crash
# https://groups.google.com/group/spyderlib/browse_thread/thread/2257abf530e210bd
QTimer.singleShot(50, self.add_current_text_if_valid)
QComboBox.focusOutEvent(self, event)

# --- own methods
def handle_keypress(self):
"""When hitting tab, it handles if single or double tab"""
if self.numpress == 2:
self.sig_double_tab_pressed.emit(True)
self.numpress = 0

def is_valid(self, qstr):
"""
Return True if string is valid
Return None if validation can't be done
"""
pass

def selected(self):
"""Action to be executed when a valid item has been selected"""
self.valid.emit(True)
self.valid.emit(True, True)

def add_text(self, text):
"""Add text to combo box: add a new item if text is not found in
combo box items"""
"""Add text to combo box: add a new item if text is not found in
combo box items."""
index = self.findText(text)
while index != -1:
self.removeItem(index)
Expand All @@ -77,18 +107,32 @@ def add_text(self, text):
self.setCurrentIndex(0)
else:
self.setCurrentIndex(0)


def set_current_text(self, text):
"""Sets the text of the QLineEdit of the QComboBox."""
self.lineEdit().setText(to_text_string(text))

def add_current_text(self):
"""Add current text to combo box history (convenient method)"""
self.add_text(self.currentText())

text = self.currentText()
if osp.isdir(text):
if text[-1] == os.sep:
text = text[:-1]
self.add_text(text)

def add_current_text_if_valid(self):
"""Add current text to combo box history if valid"""
valid = self.is_valid(self.currentText())
if valid or valid is None:
self.add_current_text()
return True

else:
self.set_current_text(self.selected_text)

def hide_completer(self):
"""Hides the completion widget."""
self.setCompleter(QCompleter([], self))


class PatternComboBox(BaseComboBox):
"""Search pattern combo box"""
Expand All @@ -108,84 +152,125 @@ class EditableComboBox(BaseComboBox):
"""
Editable combo box + Validate
"""

def __init__(self, parent):
BaseComboBox.__init__(self, parent)
self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength)
self.font = QFont()
self.selected_text = self.currentText()

# Widget setup
self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength)

# Signals
self.editTextChanged.connect(self.validate)
self.activated.connect(lambda qstr: self.validate(qstr, editing=False))
self.set_default_style()
self.tips = {True: _("Press enter to validate this entry"),
False: _('This entry is incorrect')}

def show_tip(self, tip=""):
"""Show tip"""
QToolTip.showText(self.mapToGlobal(self.pos()), tip, self)

def set_default_style(self):
"""Set widget style to default"""
self.font.setBold(False)
self.setFont(self.font)
self.setStyleSheet("")
self.show_tip()


def selected(self):
"""Action to be executed when a valid item has been selected"""
BaseComboBox.selected(self)
self.set_default_style()
self.selected_text = self.currentText()

def validate(self, qstr, editing=True):
"""Validate entered path"""
if self.selected_text == qstr:
self.valid.emit(True, True)
return

valid = self.is_valid(qstr)
if self.hasFocus() and valid is not None:
self.font.setBold(True)
self.setFont(self.font)
if editing:
if valid:
self.setStyleSheet("color:rgb(50, 155, 50);")
self.valid.emit(True, False)
else:
self.setStyleSheet("color:rgb(200, 50, 50);")
if editing:
# Combo box text is being modified: invalidate the entry
self.show_tip(self.tips[valid])
self.valid.emit(False)
else:
# A new item has just been selected
if valid:
self.selected()
else:
self.valid.emit(False)
else:
self.set_default_style()

self.valid.emit(False, False)


class PathComboBox(EditableComboBox):
"""
QComboBox handling path locations
"""
open_dir = Signal(str)

def __init__(self, parent, adjust_to_contents=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove blanks only if they're around the code you're touching. This is not a problem in this case because we almost never touch comboboxes, so it's not a blocker for merging :-)

But I've had major headaches trying to merge things between 2.3 and master when that rule is not followed :-)

My idea is to (slowly) move to remove blanks and adhere to pep8, by following Raymond Hettinger advice: https://www.youtube.com/watch?v=wf-BqAjZb8M

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I have seen that video more than once ;-)

EditableComboBox.__init__(self, parent)

# Replace the default lineedit by a custom one with icon display
lineedit = IconLineEdit(self)

# Widget setup
if adjust_to_contents:
self.setSizeAdjustPolicy(QComboBox.AdjustToContents)
else:
self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.tips = {True: _("Press enter to validate this path"),
False: _('This path is incorrect.\n'
'Enter a correct directory path,\n'
'then press enter to validate')}

False: ''}
self.setLineEdit(lineedit)

# Signals
self.sig_tab_pressed.connect(self.tab_complete)
self.sig_double_tab_pressed.connect(self.double_tab_complete)
self.valid.connect(lineedit.update_status)

# --- Qt overrides
def focusInEvent(self, event):
"""Handle focus in event restoring to display the status icon."""
show_status = getattr(self.lineEdit(), 'show_status_icon', None)
if show_status:
show_status()
QComboBox.focusInEvent(self, event)

def focusOutEvent(self, event):
"""Handle focus out event restoring the last valid selected path."""
# Calling asynchronously the 'add_current_text' to avoid crash
# https://groups.google.com/group/spyderlib/browse_thread/thread/2257abf530e210bd
lineedit = self.lineEdit()
QTimer.singleShot(50, lambda: lineedit.setText(self.selected_text))

hide_status = getattr(self.lineEdit(), 'hide_status_icon', None)
if hide_status:
hide_status()
QComboBox.focusOutEvent(self, event)

# --- Own methods
def _complete_options(self):
"""Find available completion options."""
text = to_text_string(self.currentText())
opts = glob.glob(text + "*")
opts = sorted([opt for opt in opts if osp.isdir(opt)])
self.setCompleter(QCompleter(opts, self))
return opts

def double_tab_complete(self):
"""If several options available a double tab displays options."""
opts = self._complete_options()
if len(opts) > 1:
self.completer().complete()

def tab_complete(self):
"""
If there is a single option available one tab completes the option.
"""
opts = self._complete_options()
if len(opts) == 1:
self.set_current_text(opts[0] + os.sep)
self.hide_completer()

def is_valid(self, qstr=None):
"""Return True if string is valid"""
if qstr is None:
qstr = self.currentText()
return osp.isdir( to_text_string(qstr) )
return osp.isdir(to_text_string(qstr))

def selected(self):
"""Action to be executed when a valid item has been selected"""
EditableComboBox.selected(self)
self.open_dir.emit(self.currentText())
self.selected_text = self.currentText()
self.valid.emit(True, True)
self.open_dir.emit(self.selected_text)


class UrlComboBox(PathComboBox):
Expand All @@ -195,7 +280,7 @@ class UrlComboBox(PathComboBox):
def __init__(self, parent, adjust_to_contents=False):
PathComboBox.__init__(self, parent, adjust_to_contents)
self.editTextChanged.disconnect(self.validate)

def is_valid(self, qstr=None):
"""Return True if string is valid"""
if qstr is None:
Expand All @@ -217,13 +302,13 @@ class PythonModulesComboBox(PathComboBox):
"""
def __init__(self, parent, adjust_to_contents=False):
PathComboBox.__init__(self, parent, adjust_to_contents)

def is_valid(self, qstr=None):
"""Return True if string is valid"""
if qstr is None:
qstr = self.currentText()
return is_module_or_package(to_text_string(qstr))

def selected(self):
"""Action to be executed when a valid item has been selected"""
EditableComboBox.selected(self)
Expand Down
Loading