Reputation: 140
I know that for QComboBox there is method called editable with which you can manually edit current value:
combo.setEditable(True)
. I could not find something similar with QDateEdit. I want user be able to delete entire date string and manually enter just a year value or leave it blank.
Upvotes: 3
Views: 1112
Reputation: 48231
Editing such as what is requested is not possible, since QDateEdit (which is based on QDateTimeEdit) inherits from QAbstractSpinBox, which already contains a QLineEdit widget, but has strict rules about what can be typed.
While subclassing QDateEdit is possible, it could be a bit complex, as it uses advanced controls (most importantly the "current section", which tells what part of the date is being edited). Switching date formats ("yyyy-MM-dd" and "yyyy") is possible, but not automatically, and it would take lots of checking, possibly with regex and advanced text cursor control.
In my experience, changing the keyboard behavior of QDateTimeEdit classes is really hard to achieve without bugs or unexpected behavior, and since the main features of the spinbox (arrows up/down and up/down arrow keys) are not required here, you can create a control that embeds both a QLineEdit and a child QCalendarWidget that is opened as a popup using a button.
I also implemented a small validator to ignore most invalid inputs (but you could still type an invalid date, for example 2020-31-31).
class SimpleDateValidator(QtGui.QValidator):
def validate(self, text, pos):
if not text:
return self.Acceptable, text, pos
fmt = self.parent().format()
_sep = set(fmt.replace('y', '').replace('M', '').replace('d', ''))
for l in text:
# ensure that the typed text is either a digit or a separator
if not l.isdigit() and l not in _sep:
return self.Invalid, text, pos
years = fmt.count('y')
if len(text) <= years and text.isdigit():
return self.Acceptable, text, pos
if QtCore.QDate.fromString(text, fmt).isValid():
return self.Acceptable, text, pos
return self.Intermediate, text, pos
class DateEdit(QtWidgets.QWidget):
customFormat = 'yyyy-MM-dd'
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.lineEdit = QtWidgets.QLineEdit()
layout.addWidget(self.lineEdit)
self.lineEdit.setMaxLength(len(self.format()))
self.validator = SimpleDateValidator(self)
self.lineEdit.setValidator(self.validator)
self.dropDownButton = QtWidgets.QToolButton()
layout.addWidget(self.dropDownButton)
self.dropDownButton.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_ArrowDown))
self.dropDownButton.setMaximumHeight(self.lineEdit.sizeHint().height())
self.dropDownButton.setCheckable(True)
self.dropDownButton.setFocusPolicy(QtCore.Qt.NoFocus)
self.calendar = QtWidgets.QCalendarWidget()
self.calendar.setWindowFlags(QtCore.Qt.Popup)
self.calendar.installEventFilter(self)
self.dropDownButton.pressed.connect(self.showPopup)
self.dropDownButton.released.connect(self.calendar.hide)
self.lineEdit.editingFinished.connect(self.editingFinished)
self.calendar.clicked.connect(self.setDate)
self.calendar.activated.connect(self.setDate)
self.setDate(QtCore.QDate.currentDate())
def editingFinished(self):
# optional: clear the text if the date is not valid when leaving focus;
# this will only work if *NO* validator is set
if self.calendar.isVisible():
return
if not self.isValid():
self.lineEdit.setText('')
def format(self):
return self.customFormat or QtCore.QLocale().dateFormat(QtCore.QLocale.ShortFormat)
def setFormat(self, format):
# only accept numeric date formats
if format and 'MMM' in format or 'ddd' in format:
return
self.customFormat = format
self.setDate(self.calendar.selectedDate())
self.calendar.hide()
self.lineEdit.setMaxLength(self.format())
self.validator.setFormat(self.format())
def text(self):
return self.lineEdit.text()
def date(self):
if not self.isValid():
return None
date = QtCore.QDate.fromString(self.text(), self.format())
if date.isValid():
return date
return int(self.text())
def setDate(self, date):
self.lineEdit.setText(date.toString(self.format()))
self.calendar.setSelectedDate(date)
self.calendar.hide()
def setDateRange(self, minimum, maximum):
self.calendar.setDateRange(minimum, maximum)
def isValid(self):
text = self.text()
if not text:
return False
date = QtCore.QDate.fromString(text, self.format())
if date.isValid():
self.setDate(date)
return True
try:
year = int(text)
start = self.calendar.minimumDate().year()
end = self.calendar.maximumDate().year()
if start <= year <= end:
return True
except:
pass
return False
def hidePopup(self):
self.calendar.hide()
def showPopup(self):
pos = self.lineEdit.mapToGlobal(self.lineEdit.rect().bottomLeft())
pos += QtCore.QPoint(0, 1)
rect = QtCore.QRect(pos, self.calendar.sizeHint())
self.calendar.setGeometry(rect)
self.calendar.show()
self.calendar.setFocus()
def eventFilter(self, source, event):
# press or release the button when the calendar is shown/hidden
if event.type() == QtCore.QEvent.Hide:
self.dropDownButton.setDown(False)
elif event.type() == QtCore.QEvent.Show:
self.dropDownButton.setDown(True)
return super().eventFilter(source, event)
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Down, QtCore.Qt.Key_F4):
if not self.calendar.isVisible():
self.showPopup()
super().keyPressEvent(event)
Upvotes: 1