Merge from custcol trunk

This commit is contained in:
Charles Haley 2010-05-05 10:31:44 +01:00
commit 7f32c89948
8 changed files with 961 additions and 644 deletions

View File

@ -6,7 +6,7 @@ from threading import RLock
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal
QEvent, QTimer, pyqtSignal, QDate
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QApplication, QDialog, QPushButton
@ -21,6 +21,7 @@ from calibre.ebooks.metadata import MetaInformation
gprefs = JSONConfig('gui')
NONE = QVariant() #: Null value to return from the data function of item models
UNDEFINED_DATE = QDate(101,1,1)
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate']

View File

@ -0,0 +1,270 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
QSpacerItem, QIcon
from calibre.utils.date import qt_to_dt
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
from calibre.gui2 import UNDEFINED_DATE
from calibre.utils.config import tweaks
class Base(object):
def __init__(self, db, col_id):
self.db, self.col_id = db, col_id
self.col_metadata = db.custom_column_num_map[col_id]
self.initial_val = None
def initialize(self, book_id):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.initial_val = val
val = self.normalize_db_val(val)
self.setter(val)
def commit(self, book_id, notify=False):
val = self.getter()
val = self.normalize_ui_val(val)
if val != self.initial_val:
self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
def normalize_db_val(self, val):
return val
def normalize_ui_val(self, val):
return val
class Bool(Base):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
self.widgets = [QLabel('&'+self.col_metadata['name'], parent),
QComboBox(parent)]
w = self.widgets[1]
items = [_('Yes'), _('No'), _('Undefined')]
icons = [I('ok.svg'), I('list_remove.svg'), I('blank.svg')]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
items = items[:-1]
icons = icons[:-1]
for icon, text in zip(icons, items):
w.addItem(QIcon(icon), text)
def setter(self, val):
val = {None: 2, False: 1, True: 0}[val]
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val == 2:
val = 1
self.widgets[1].setCurrentIndex(val)
def getter(self):
val = self.widgets[1].currentIndex()
return {2: None, 1: False, 0: True}[val]
class Int(Base):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QSpinBox(parent)]
w = self.widgets[1]
w.setRange(-100, sys.maxint)
w.setSpecialValueText(_('Undefined'))
w.setSingleStep(1)
def setter(self, val):
if val is None:
val = self.widgets[1].minimum()
else:
val = int(val)
self.widgets[1].setValue(val)
def getter(self):
val = self.widgets[1].value()
if val == self.widgets[1].minimum():
val = None
return val
class Float(Int):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QDoubleSpinBox(parent)]
w = self.widgets[1]
self.setRange(-100., float(sys.maxint))
w.setDecimals(2)
class Rating(Int):
def __init__(self, db, col_id, parent=None):
Int.__init__(self, db, col_id)
w = self.widgets[1]
w.setRange(0, 5)
w.setSuffix(' '+_('stars'))
w.setSpecialValueText(_('Unrated'))
def setter(self, val):
if val is None:
val = 0
self.widgets[1].setValue(int(round(val/2.)))
def getter(self):
val = self.widgets[1].value()
if val == 0:
val = None
else:
val *= 2
return val
class DateTime(Base):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QDateEdit(parent)]
w = self.widgets[1]
w.setDisplayFormat('dd MMM yyyy')
w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_DATE)
w.setSpecialValueText(_('Undefined'))
def setter(self, val):
if val is None:
val = self.widgets[1].minimumDate()
else:
val = QDate(val.year, val.month, val.day)
self.widgets[1].setDate(val)
def getter(self):
val = self.widgets[1].date()
if val == UNDEFINED_DATE:
val = None
else:
val = qt_to_dt(val)
return val
class Comments(Base):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
self._box = QGroupBox(parent)
self._box.setTitle('&'+self.col_metadata['name'])
self._layout = QVBoxLayout()
self._tb = QPlainTextEdit(self._box)
self._tb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
self._layout.addWidget(self._tb)
self._box.setLayout(self._layout)
self.widgets = [self._box]
def setter(self, val):
if val is None:
val = ''
self._tb.setPlainText(val)
def getter(self):
val = unicode(self._tb.toPlainText()).strip()
if not val:
val = None
return val
class Text(Base):
def __init__(self, db, col_id, parent=None):
Base.__init__(self, db, col_id)
values = self.all_values = list(self.db.all_custom(num=col_id))
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
if self.col_metadata['is_multiple']:
w = TagsLineEdit(parent, values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
else:
w = EnComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
w]
def initialize(self, book_id):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.initial_val = val
val = self.normalize_db_val(val)
if self.col_metadata['is_multiple']:
self.setter(val)
self.widgets[1].update_tags_cache(self.all_values)
else:
idx = None
for i, c in enumerate(self.all_values):
if c == val:
idx = i
self.widgets[1].addItem(c)
self.widgets[1].setEditText('')
if idx is not None:
self.widgets[1].setCurrentIndex(idx)
def setter(self, val):
if self.col_metadata['is_multiple']:
if not val:
val = []
self.widgets[1].setText(u', '.join(val))
def getter(self):
if self.col_metadata['is_multiple']:
val = unicode(self.widgets[1].text()).strip()
return [x.strip() for x in val.split(',')]
val = unicode(self.widgets[1].currentText()).strip()
if not val:
val = None
return val
widgets = {
'bool' : Bool,
'rating' : Rating,
'int': Int,
'float': Float,
'datetime': DateTime,
'text' : Text,
'comments': Comments,
}
def populate_single_metadata_page(left, right, db, book_id, parent=None):
x = db.custom_column_num_map
cols = list(x)
cols.sort(cmp=lambda z,y: cmp(x[z]['name'].lower(), x[y]['name'].lower()))
ans = []
for i, col in enumerate(cols):
w = widgets[x[col]['datatype']](db, col, parent)
ans.append(w)
w.initialize(book_id)
layout = left if i%2 == 0 else right
row = layout.rowCount()
if len(w.widgets) == 1:
layout.addWidget(w.widgets[0], row, 0, 1, -1)
else:
w.widgets[0].setBuddy(w.widgets[1])
for c, widget in enumerate(w.widgets):
layout.addWidget(widget, row, c)
items = []
if len(ans) > 0:
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
QSizePolicy.Expanding))
left.addItem(items[-1], left.rowCount(), 0, 1, 1)
left.setRowStretch(left.rowCount()-1, 100)
if len(ans) > 1:
items.append(QSpacerItem(10, 100, QSizePolicy.Minimum,
QSizePolicy.Expanding))
right.addItem(items[-1], left.rowCount(), 0, 1, 1)
right.setRowStretch(right.rowCount()-1, 100)
return ans, items

View File

@ -11,8 +11,9 @@ import re
import time
import traceback
import sip
from PyQt4.Qt import SIGNAL, QObject, QCoreApplication, Qt, QTimer, QThread, QDate, \
QPixmap, QListWidgetItem, QDialog
QPixmap, QListWidgetItem, QDialog, QHBoxLayout, QGridLayout
from calibre.gui2 import error_dialog, file_icon_provider, \
choose_files, choose_images, ResizableDialog, \
@ -31,6 +32,7 @@ from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.dialogs.config.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_single_metadata_page
class CoverFetcher(QThread):
@ -405,6 +407,26 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover.setPixmap(pm)
self.cover_data = cover
self.original_series_name = unicode(self.series.text()).strip()
if len(db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
else:
self.create_custom_column_editors()
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
top_layout = QHBoxLayout()
top_layout.setSpacing(20)
left_layout = QGridLayout()
right_layout = QGridLayout()
top_layout.addLayout(left_layout)
self.custom_column_widgets, self.__cc_spacers = populate_single_metadata_page(
left_layout, right_layout, self.db, self.id, w)
top_layout.addLayout(right_layout)
sip.delete(w.layout())
w.setLayout(top_layout)
self.__custom_col_layouts = [top_layout, left_layout, right_layout]
def validate_isbn(self, isbn):
isbn = unicode(isbn).strip()
@ -675,6 +697,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.db.set_cover(self.id, self.cover_data)
else:
self.db.remove_cover(self.id)
for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.id)
except IOError, err:
if err.errno == 13: # Permission denied
fname = err.filename if err.filename else 'file'

View File

@ -43,8 +43,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>869</width>
<height>698</height>
<width>879</width>
<height>711</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
@ -52,13 +52,20 @@
<number>0</number>
</property>
<item>
<widget class="QWidget" name="central_widget" native="true">
<widget class="QTabWidget" name="central_widget">
<property name="minimumSize">
<size>
<width>800</width>
<height>665</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="central_tabWidgetPage1">
<attribute name="title">
<string>&amp;Basic metadata</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QSplitter" name="splitter">
@ -672,6 +679,13 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Custom metadata</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2"/>
</widget>
</widget>
</item>
</layout>
</widget>

View File

@ -12,14 +12,14 @@ from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QImage, QMenu, \
QStyledItemDelegate, QCompleter, QIntValidator, \
QDoubleValidator, QCheckBox
QDoubleValidator, QComboBox
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
SIGNAL, QObject, QSize, QModelIndex, QDate
from calibre import strftime
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, authors_to_string
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.gui2 import NONE, TableView, config, error_dialog
from calibre.gui2 import NONE, TableView, config, error_dialog, UNDEFINED_DATE
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
@ -29,6 +29,7 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import SearchQueryParser
class RatingDelegate(QStyledItemDelegate):
COLOR = QColor("blue")
SIZE = 16
@ -79,14 +80,14 @@ class RatingDelegate(QStyledItemDelegate):
painter.setRenderHint(QPainter.Antialiasing)
painter.setClipRect(option.rect)
y = option.rect.center().y()-self.SIZE/2.
x = option.rect.right() - self.SIZE
x = option.rect.left()
painter.setPen(self.PEN)
painter.setBrush(self.brush)
painter.translate(x, y)
i = 0
while i < num:
draw_star()
painter.translate(-self.SIZE, 0)
painter.translate(self.SIZE, 0)
i += 1
except:
traceback.print_exc()
@ -99,8 +100,11 @@ class RatingDelegate(QStyledItemDelegate):
return sb
class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
d = val.toDate()
if d == UNDEFINED_DATE:
return ''
return d.toString('dd MMM yyyy')
def createEditor(self, parent, option, index):
@ -109,18 +113,24 @@ class DateDelegate(QStyledItemDelegate):
if 'yyyy' not in stdformat:
stdformat = stdformat.replace('yy', 'yyyy')
qde.setDisplayFormat(stdformat)
qde.setMinimumDate(QDate(101,1,1))
qde.setMinimumDate(UNDEFINED_DATE)
qde.setSpecialValueText(_('Undefined'))
qde.setCalendarPopup(True)
return qde
class PubDateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
return val.toDate().toString('MMM yyyy')
d = val.toDate()
if d == UNDEFINED_DATE:
return ''
return d.toString('MMM yyyy')
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('MM yyyy')
qde.setMinimumDate(QDate(101,1,1))
qde.setMinimumDate(UNDEFINED_DATE)
qde.setSpecialValueText(_('Undefined'))
qde.setCalendarPopup(True)
return qde
@ -217,16 +227,19 @@ class CcBoolDelegate(QStyledItemDelegate):
QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
editor = QCheckBox(parent)
editor = QComboBox(parent)
items = [_('Y'), _('N'), ' ']
icons = [I('ok.svg'), I('list_remove.svg'), I('blank.svg')]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
pass
else:
if tweaks['bool_custom_columns_are_tristate'] == 'yes':
editor.setTristate(True)
items = items[:-1]
icons = icons[:-1]
for icon, text in zip(icons, items):
editor.addItem(QIcon(icon), text)
return editor
def setModelData(self, editor, model, index):
model.setData(index, QVariant(editor.checkState()), Qt.EditRole)
val = {0:True, 1:False, 2:None}[editor.currentIndex()]
model.setData(index, QVariant(val), Qt.EditRole)
def setEditorData(self, editor, index):
m = index.model()
@ -234,10 +247,10 @@ class CcBoolDelegate(QStyledItemDelegate):
# gui column -> column label -> table number -> db column
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
val = Qt.Unchecked if val is None or not val else Qt.Checked
val = 1 if not val else 0
else:
val = Qt.PartiallyChecked if val is None else Qt.Unchecked if not val else Qt.Checked
editor.setCheckState(val)
val = 2 if val is None else 1 if not val else 0
editor.setCurrentIndex(val)
class BooksModel(QAbstractTableModel):
@ -693,7 +706,7 @@ class BooksModel(QAbstractTableModel):
if val is not None:
return QVariant(QDate(val))
else:
return QVariant(QDate())
return QVariant(UNDEFINED_DATE)
def bool_type(r, idx=-1):
return None # displayed using a decorator
@ -817,8 +830,7 @@ class BooksModel(QAbstractTableModel):
val = unicode(value.toString()).strip()
val = val if val else None
if typ == 'bool':
val = value.toInt()[0] # tristate checkboxes put unknown in the middle
val = None if val == 1 else False if val == 0 else True
val = value.toPyObject()
elif typ == 'rating':
val = value.toInt()[0]
val = 0 if val < 0 else 5 if val > 5 else val

View File

@ -160,7 +160,7 @@
<item>
<widget class="QLabel" name="restriction_label">
<property name="text">
<string>&amp;Restrict display to:</string>
<string>&amp;Restrict to:</string>
</property>
<property name="buddy">
<cstring>search_restriction</cstring>
@ -352,7 +352,6 @@
</property>
</widget>
</item>
<item>
<item>
<widget class="QCheckBox" name="popularity">
<property name="text">
@ -360,7 +359,6 @@
</property>
</widget>
</item>
</item>
<item>
<widget class="QComboBox" name="tag_match">
<property name="currentIndex">
@ -378,7 +376,6 @@
</item>
</widget>
</item>
<item>
<item>
<widget class="QPushButton" name="edit_categories">
<property name="toolTip">
@ -389,7 +386,6 @@
</property>
</widget>
</item>
</item>
</layout>
</widget>
<widget class="BooksView" name="library_view">

View File

@ -73,7 +73,7 @@ class SearchBox2(QComboBox):
self.setInsertPolicy(self.NoInsert)
self.setMaxCount(self.MAX_COUNT)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(50)
self.setMinimumContentsLength(25)
def initialize(self, opt_name, colorize=False,
help_text=_('Search')):

View File

@ -530,7 +530,7 @@ class BasicList(QListWidget):
class LineEditECM(object):
'''
Extend the contenxt menu of a QLineEdit to include more actions.
Extend the context menu of a QLineEdit to include more actions.
'''
def contextMenuEvent(self, event):
@ -659,7 +659,7 @@ class EnComboBox(QComboBox):
'''
Enhanced QComboBox.
Includes an extended content menu.
Includes an extended context menu.
'''
def __init__(self, *args):