Allow copy/pasting dates from columns having different date display formats. Fixes #1885004 [Enhancement Request: Copy & paste dates between fields of unlike formats](https://bugs.launchpad.net/calibre/+bug/1885004)

Also consolidate all date/time editor to use the same underlying
widget.
This commit is contained in:
Kovid Goyal 2020-06-25 12:44:59 +05:30
parent c6b4140231
commit 9b3c57062b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 120 additions and 92 deletions

View File

@ -9,10 +9,10 @@ __docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt5.Qt import (QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateTimeEdit,
from PyQt5.Qt import (QComboBox, QLabel, QSpinBox, QDoubleSpinBox,
QDateTime, QGroupBox, QVBoxLayout, QSizePolicy, QGridLayout, QUrl,
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, QLineEdit,
QPushButton, QMessageBox, QToolButton, Qt, QPlainTextEdit)
QPushButton, QMessageBox, QToolButton, QPlainTextEdit)
from calibre.utils.date import qt_to_dt, now, as_local_time, as_utc, internal_iso_format_string
from calibre.gui2.complete2 import EditWithComplete
@ -23,7 +23,7 @@ from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html
from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox
from calibre.gui2.widgets2 import RatingEditor
from calibre.gui2.widgets2 import RatingEditor, DateTimeEdit as DateTimeEditBase
from polyglot.builtins import unicode_type
@ -275,15 +275,15 @@ class Rating(Base):
self.signals_to_disconnect.append(self.widgets[1].currentTextChanged)
class DateTimeEdit(QDateTimeEdit):
class DateTimeEdit(DateTimeEditBase):
def focusInEvent(self, x):
self.setSpecialValueText('')
QDateTimeEdit.focusInEvent(self, x)
DateTimeEditBase.focusInEvent(self, x)
def focusOutEvent(self, x):
self.setSpecialValueText(_('Undefined'))
QDateTimeEdit.focusOutEvent(self, x)
DateTimeEditBase.focusOutEvent(self, x)
def set_to_today(self):
self.setDateTime(now())
@ -292,16 +292,6 @@ class DateTimeEdit(QDateTimeEdit):
self.setDateTime(now())
self.setDateTime(UNDEFINED_QDATETIME)
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Minus:
ev.accept()
self.setDateTime(self.minimumDateTime())
elif ev.key() == Qt.Key_Equal:
ev.accept()
self.setDateTime(QDateTime.currentDateTime())
else:
return QDateTimeEdit.keyPressEvent(self, ev)
class DateTime(Base):

View File

@ -26,7 +26,6 @@ from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.gui2.metadata.basic_widgets import CalendarWidget
from calibre.utils.config import JSONConfig, dynamic, prefs, tweaks
from calibre.utils.date import qt_to_dt, internal_iso_format_string
from calibre.utils.icu import capitalize, sort_key
@ -503,10 +502,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.series.editTextChanged.connect(self.series_changed)
self.tag_editor_button.clicked.connect(self.tag_editor)
self.autonumber_series.stateChanged[int].connect(self.auto_number_changed)
self.pubdate.setMinimumDateTime(UNDEFINED_QDATETIME)
self.pubdate_cw = CalendarWidget(self.pubdate)
self.pubdate_cw.setVerticalHeaderFormat(self.pubdate_cw.NoVerticalHeader)
self.pubdate.setCalendarWidget(self.pubdate_cw)
pubdate_format = tweaks['gui_pubdate_display_format']
if pubdate_format == 'iso':
pubdate_format = internal_iso_format_string()
@ -516,10 +511,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
self.pubdate.dateTimeChanged.connect(self.do_apply_pubdate)
self.adddate.setDateTime(QDateTime.currentDateTime())
self.adddate.setMinimumDateTime(UNDEFINED_QDATETIME)
self.adddate_cw = CalendarWidget(self.adddate)
self.adddate_cw.setVerticalHeaderFormat(self.adddate_cw.NoVerticalHeader)
self.adddate.setCalendarWidget(self.adddate_cw)
adddate_format = tweaks['gui_timestamp_display_format']
if adddate_format == 'iso':
adddate_format = internal_iso_format_string()

View File

@ -401,7 +401,7 @@ from the value in the box</string>
<item row="10" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QDateTimeEdit" name="pubdate">
<widget class="DateTimeEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
@ -445,7 +445,7 @@ from the value in the box</string>
<item row="9" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QDateTimeEdit" name="adddate">
<widget class="DateTimeEdit" name="adddate">
<property name="displayFormat">
<string>d MMM yyyy</string>
</property>
@ -726,8 +726,8 @@ for e.g., EPUB to EPUB, calibre saves the original EPUB
<rect>
<x>0</x>
<y>0</y>
<width>935</width>
<height>639</height>
<width>777</width>
<height>388</height>
</rect>
</property>
<layout class="QGridLayout" name="vargrid">
@ -1205,8 +1205,8 @@ not multiple and the destination field is multiple</string>
<rect>
<x>0</x>
<y>0</y>
<width>917</width>
<height>319</height>
<width>203</width>
<height>70</height>
</rect>
</property>
<layout class="QGridLayout" name="testgrid">
@ -1329,6 +1329,11 @@ is completed. This can be slow on large libraries.</string>
<extends>QComboBox</extends>
<header>calibre/gui2/widgets2.h</header>
</customwidget>
<customwidget>
<class>DateTimeEdit</class>
<extends>QDateTimeEdit</extends>
<header>calibre/gui2/widgets2.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>central_widget</tabstop>

View File

@ -17,7 +17,7 @@ from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import UNDEFINED_QDATETIME, rating_font
from calibre.constants import iswindows
from calibre.gui2.widgets import EnLineEdit
from calibre.gui2.widgets2 import populate_standard_spinbox_context_menu, RatingEditor
from calibre.gui2.widgets2 import populate_standard_spinbox_context_menu, RatingEditor, DateTimeEdit as DateTimeEditBase
from calibre.gui2.complete2 import EditWithComplete
from calibre.utils.date import now, format_date, qt_to_dt, is_date_undefined, internal_iso_format_string
@ -112,43 +112,15 @@ class UpdateEditorGeometry(object):
editor.setGeometry(initial_geometry)
class DateTimeEdit(QDateTimeEdit): # {{{
class DateTimeEdit(DateTimeEditBase): # {{{
def __init__(self, parent, format_):
QDateTimeEdit.__init__(self, parent)
DateTimeEditBase.__init__(self, parent)
self.setFrame(False)
self.setMinimumDateTime(UNDEFINED_QDATETIME)
self.setSpecialValueText(_('Undefined'))
self.setCalendarPopup(True)
if format_ == 'iso':
format_ = internal_iso_format_string()
self.setDisplayFormat(format_)
def contextMenuEvent(self, ev):
m = QMenu(self)
m.addAction(_('Set date to undefined') + '\t' + QKeySequence(Qt.Key_Minus).toString(QKeySequence.NativeText),
self.clear_date)
m.addAction(_('Set date to today') + '\t' + QKeySequence(Qt.Key_Equal).toString(QKeySequence.NativeText),
self.today_date)
m.addSeparator()
populate_standard_spinbox_context_menu(self, m)
m.popup(ev.globalPos())
def today_date(self):
self.setDateTime(QDateTime.currentDateTime())
def clear_date(self):
self.setDateTime(UNDEFINED_QDATETIME)
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Minus:
ev.accept()
self.clear_date()
elif ev.key() == Qt.Key_Equal:
self.today_date()
ev.accept()
else:
return QDateTimeEdit.keyPressEvent(self, ev)
# }}}
# Number Editor {{{

View File

@ -10,20 +10,20 @@ import textwrap, re, os, shutil, weakref
from datetime import date, datetime
from PyQt5.Qt import (
Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget,
Qt, pyqtSignal, QMessageBox, QIcon, QToolButton, QWidget,
QLabel, QGridLayout, QApplication, QDoubleSpinBox, QListWidgetItem, QSize,
QPixmap, QDialog, QMenu, QLineEdit, QSizePolicy, QKeySequence,
QDialogButtonBox, QAction, QCalendarWidget, QDate, QDateTime, QUndoCommand,
QDialogButtonBox, QAction, QDateTime, QUndoCommand,
QUndoStack, QVBoxLayout, QPlainTextEdit, QUrl)
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
from calibre.gui2.widgets2 import access_key, populate_standard_spinbox_context_menu, RightClickButton, Dialog, RatingEditor
from calibre.gui2.widgets2 import access_key, populate_standard_spinbox_context_menu, RightClickButton, Dialog, RatingEditor, DateTimeEdit
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import (
title_sort, string_to_authors, check_isbn, authors_to_sort_string)
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATETIME,
from calibre.gui2 import (file_icon_provider,
choose_files, error_dialog, choose_images, gprefs)
from calibre.gui2.complete2 import EditWithComplete
from calibre.utils.date import (
@ -1797,14 +1797,7 @@ class PublisherEdit(EditWithComplete, ToMetadataMixin): # {{{
# DateEdit {{{
class CalendarWidget(QCalendarWidget):
def showEvent(self, ev):
if self.selectedDate().year() == UNDEFINED_DATE.year:
self.setSelectedDate(QDate.currentDate())
class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin):
class DateEdit(make_undoable(DateTimeEdit), ToMetadataMixin):
TOOLTIP = ''
LABEL = _('&Date:')
@ -1824,12 +1817,6 @@ class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin):
elif fmt == 'iso':
fmt = internal_iso_format_string()
self.setDisplayFormat(fmt)
self.setCalendarPopup(True)
self.cw = CalendarWidget(self)
self.cw.setVerticalHeaderFormat(self.cw.NoVerticalHeader)
self.setCalendarWidget(self.cw)
self.setMinimumDateTime(UNDEFINED_QDATETIME)
self.setSpecialValueText(_('Undefined'))
if create_clear_button:
self.clear_button = QToolButton(parent)
self.clear_button.setIcon(QIcon(I('trash.png')))
@ -1867,13 +1854,7 @@ class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin):
return o != c
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Minus:
ev.accept()
self.setDateTime(self.minimumDateTime())
elif ev.key() == Qt.Key_Equal:
ev.accept()
self.setDateTime(QDateTime.currentDateTime())
elif ev.key() == Qt.Key_Up and is_date_undefined(self.current_val):
if ev.key() == Qt.Key_Up and is_date_undefined(self.current_val):
self.setDateTime(QDateTime.currentDateTime())
elif ev.key() == Qt.Key_Tab and is_date_undefined(self.current_val):
ev.ignore()

View File

@ -7,18 +7,20 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import weakref
from PyQt5.Qt import (
QApplication, QCheckBox, QColor, QColorDialog, QComboBox, QDialog,
QDialogButtonBox, QFont, QFontInfo, QFontMetrics, QIcon, QKeySequence, QLabel,
QLayout, QPalette, QPixmap, QPoint, QPushButton, QRect, QScrollArea, QSize,
QApplication, QByteArray, QCalendarWidget, QCheckBox, QColor, QColorDialog,
QComboBox, QDate, QDateTime, QDateTimeEdit, QDialog, QDialogButtonBox, QFont,
QFontInfo, QFontMetrics, QIcon, QKeySequence, QLabel, QLayout, QLineEdit, QMenu,
QMimeData, QPalette, QPixmap, QPoint, QPushButton, QRect, QScrollArea, QSize,
QSizePolicy, QStyle, QStyledItemDelegate, Qt, QTabWidget, QTextBrowser,
QToolButton, QUndoCommand, QUndoStack, QWidget, pyqtSignal, QByteArray
QToolButton, QUndoCommand, QUndoStack, QWidget, pyqtSignal, pyqtSlot
)
from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import gprefs, rating_font
from calibre.gui2 import UNDEFINED_QDATETIME, gprefs, rating_font
from calibre.gui2.complete2 import EditWithComplete, LineEdit
from calibre.gui2.widgets import history
from calibre.utils.config_base import tweaks
from calibre.utils.date import UNDEFINED_DATE
from polyglot.builtins import unicode_type
from polyglot.functools import lru_cache
@ -555,6 +557,93 @@ def to_plain_text(self):
return ans.rstrip('\0')
class LineEditForDateTimeEdit(QLineEdit):
date_time_pasted = pyqtSignal(object)
date_time_copied = pyqtSignal(object)
MIME_TYPE = 'application/x-calibre-datetime-value'
@pyqtSlot()
def copy(self):
self.date_time_copied.emit(self.selectedText())
@pyqtSlot()
def paste(self):
md = QApplication.instance().clipboard().mimeData()
if md.hasFormat(self.MIME_TYPE):
self.date_time_pasted.emit(QDateTime.fromString(md.data(self.MIME_TYPE).data().decode('ascii'), Qt.ISODate))
else:
QLineEdit.paste(self)
class CalendarWidget(QCalendarWidget):
def showEvent(self, ev):
if self.selectedDate().year() == UNDEFINED_DATE.year:
self.setSelectedDate(QDate.currentDate())
class DateTimeEdit(QDateTimeEdit):
def __init__(self, parent=None):
QDateTimeEdit.__init__(self, parent)
le = LineEditForDateTimeEdit(self)
self.setLineEdit(le)
le.date_time_pasted.connect(self.date_time_pasted, type=Qt.QueuedConnection)
le.date_time_copied.connect(self.date_time_copied, type=Qt.QueuedConnection)
self.setMinimumDateTime(UNDEFINED_QDATETIME)
self.setCalendarPopup(True)
self.cw = CalendarWidget(self)
self.cw.setVerticalHeaderFormat(self.cw.NoVerticalHeader)
self.setCalendarWidget(self.cw)
self.setSpecialValueText(_('Undefined'))
def date_time_copied(self, text):
md = QMimeData()
md.setText(text or self.dateTime().toString())
md.setData(LineEditForDateTimeEdit.MIME_TYPE, self.dateTime().toString(Qt.ISODate).encode('ascii'))
QApplication.instance().clipboard().setMimeData(md)
def date_time_pasted(self, qt_dt):
self.setDateTime(qt_dt)
def create_context_menu(self):
m = QMenu(self)
m.addAction(_('Set date to undefined') + '\t' + QKeySequence(Qt.Key_Minus).toString(QKeySequence.NativeText),
self.clear_date)
m.addAction(_('Set date to today') + '\t' + QKeySequence(Qt.Key_Equal).toString(QKeySequence.NativeText),
self.today_date)
m.addSeparator()
populate_standard_spinbox_context_menu(self, m)
return m
def contextMenuEvent(self, ev):
m = self.create_context_menu()
m.popup(ev.globalPos())
def today_date(self):
self.setDateTime(QDateTime.currentDateTime())
def clear_date(self):
self.setDateTime(UNDEFINED_QDATETIME)
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Minus:
ev.accept()
self.clear_date()
elif ev.key() == Qt.Key_Equal:
self.today_date()
ev.accept()
elif ev.matches(QKeySequence.Copy):
self.lineEdit().copy()
ev.accept()
elif ev.matches(QKeySequence.Paste):
self.lineEdit().paste()
ev.accept()
else:
return QDateTimeEdit.keyPressEvent(self, ev)
if __name__ == '__main__':
from calibre.gui2 import Application
app = Application([])