mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-08 06:05:04 -05:00
990 lines
35 KiB
Python
990 lines
35 KiB
Python
#!/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'
|
|
|
|
from functools import partial
|
|
|
|
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateTimeEdit, \
|
|
QDateTime, QGroupBox, QVBoxLayout, QSizePolicy, QGridLayout, \
|
|
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
|
|
QPushButton
|
|
|
|
from calibre.utils.date import qt_to_dt, now
|
|
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
|
from calibre.gui2.comments_editor import Editor as CommentsEditor
|
|
from calibre.gui2 import UNDEFINED_QDATETIME, error_dialog
|
|
from calibre.utils.config import tweaks
|
|
from calibre.utils.icu import sort_key
|
|
from calibre.library.comments import comments_to_html
|
|
|
|
class Base(object):
|
|
|
|
def __init__(self, db, col_id, parent=None):
|
|
self.db, self.col_id = db, col_id
|
|
self.col_metadata = db.custom_column_num_map[col_id]
|
|
self.initial_val = self.widgets = None
|
|
self.setup_ui(parent)
|
|
|
|
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)
|
|
|
|
@property
|
|
def gui_val(self):
|
|
return self.getter()
|
|
|
|
|
|
def commit(self, book_id, notify=False):
|
|
val = self.gui_val
|
|
val = self.normalize_ui_val(val)
|
|
if val != self.initial_val:
|
|
return self.db.set_custom(book_id, val, num=self.col_id,
|
|
notify=notify, commit=False, allow_case_change=True)
|
|
else:
|
|
return set()
|
|
|
|
def normalize_db_val(self, val):
|
|
return val
|
|
|
|
def normalize_ui_val(self, val):
|
|
return val
|
|
|
|
def break_cycles(self):
|
|
self.db = self.widgets = self.initial_val = None
|
|
|
|
class Bool(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
QComboBox(parent)]
|
|
w = self.widgets[1]
|
|
items = [_('Yes'), _('No'), _('Undefined')]
|
|
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
|
if not self.db.prefs.get('bools_are_tristate'):
|
|
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 not self.db.prefs.get('bools_are_tristate') 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 setup_ui(self, parent):
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
QSpinBox(parent)]
|
|
w = self.widgets[1]
|
|
w.setRange(-1000000, 100000000)
|
|
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 setup_ui(self, parent):
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
QDoubleSpinBox(parent)]
|
|
w = self.widgets[1]
|
|
w.setRange(-1000000., float(100000000))
|
|
w.setDecimals(2)
|
|
w.setSpecialValueText(_('Undefined'))
|
|
w.setSingleStep(1)
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
val = self.widgets[1].minimum()
|
|
self.widgets[1].setValue(val)
|
|
|
|
class Rating(Int):
|
|
|
|
def setup_ui(self, parent):
|
|
Int.setup_ui(self, parent)
|
|
w = self.widgets[1]
|
|
w.setRange(0, 5)
|
|
w.setSuffix(' '+_('star(s)'))
|
|
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 DateTimeEdit(QDateTimeEdit):
|
|
|
|
def focusInEvent(self, x):
|
|
self.setSpecialValueText('')
|
|
QDateTimeEdit.focusInEvent(self, x)
|
|
|
|
def focusOutEvent(self, x):
|
|
self.setSpecialValueText(_('Undefined'))
|
|
QDateTimeEdit.focusOutEvent(self, x)
|
|
|
|
def set_to_today(self):
|
|
self.setDateTime(now())
|
|
|
|
def set_to_clear(self):
|
|
self.setDateTime(UNDEFINED_QDATETIME)
|
|
|
|
class DateTime(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
cm = self.col_metadata
|
|
self.widgets = [QLabel('&'+cm['name']+':', parent), DateTimeEdit(parent)]
|
|
self.widgets.append(QLabel(''))
|
|
w = QWidget(parent)
|
|
self.widgets.append(w)
|
|
l = QHBoxLayout()
|
|
l.setContentsMargins(0, 0, 0, 0)
|
|
w.setLayout(l)
|
|
l.addStretch(1)
|
|
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
|
|
l.addWidget(self.today_button)
|
|
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
|
|
l.addWidget(self.clear_button)
|
|
l.addStretch(2)
|
|
|
|
w = self.widgets[1]
|
|
format = cm['display'].get('date_format','')
|
|
if not format:
|
|
format = 'dd MMM yyyy hh:mm'
|
|
w.setDisplayFormat(format)
|
|
w.setCalendarPopup(True)
|
|
w.setMinimumDateTime(UNDEFINED_QDATETIME)
|
|
w.setSpecialValueText(_('Undefined'))
|
|
self.today_button.clicked.connect(w.set_to_today)
|
|
self.clear_button.clicked.connect(w.set_to_clear)
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
val = self.widgets[1].minimumDateTime()
|
|
else:
|
|
val = QDateTime(val)
|
|
self.widgets[1].setDateTime(val)
|
|
|
|
def getter(self):
|
|
val = self.widgets[1].dateTime()
|
|
if val <= UNDEFINED_QDATETIME:
|
|
val = None
|
|
else:
|
|
val = qt_to_dt(val)
|
|
return val
|
|
|
|
class Comments(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
self._box = QGroupBox(parent)
|
|
self._box.setTitle('&'+self.col_metadata['name'])
|
|
self._layout = QVBoxLayout()
|
|
self._tb = CommentsEditor(self._box)
|
|
self._tb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
#self._tb.setTabChangesFocus(True)
|
|
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.html = comments_to_html(val)
|
|
|
|
def getter(self):
|
|
val = unicode(self._tb.html).strip()
|
|
if not val:
|
|
val = None
|
|
return val
|
|
|
|
class Text(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
self.sep = self.col_metadata['multiple_seps']
|
|
|
|
if self.col_metadata['is_multiple']:
|
|
w = MultiCompleteLineEdit(parent)
|
|
w.set_separator(self.sep['ui_to_list'])
|
|
if self.sep['ui_to_list'] == '&':
|
|
w.set_space_before_sep(True)
|
|
w.set_add_separator(tweaks['authors_completer_append_separator'])
|
|
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
|
else:
|
|
w = MultiCompleteComboBox(parent)
|
|
w.set_separator(None)
|
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
|
w.setMinimumContentsLength(25)
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
|
|
|
def initialize(self, book_id):
|
|
values = list(self.db.all_custom(num=self.col_id))
|
|
values.sort(key=sort_key)
|
|
self.widgets[1].clear()
|
|
self.widgets[1].update_items_cache(values)
|
|
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)
|
|
else:
|
|
idx = None
|
|
for i, c in enumerate(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(self.sep['list_to_ui'].join(val))
|
|
|
|
def getter(self):
|
|
if self.col_metadata['is_multiple']:
|
|
val = unicode(self.widgets[1].text()).strip()
|
|
ans = [x.strip() for x in val.split(self.sep['ui_to_list']) if x.strip()]
|
|
if not ans:
|
|
ans = None
|
|
return ans
|
|
val = unicode(self.widgets[1].currentText()).strip()
|
|
if not val:
|
|
val = None
|
|
return val
|
|
|
|
class Series(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
w = MultiCompleteComboBox(parent)
|
|
w.set_separator(None)
|
|
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
|
w.setMinimumContentsLength(25)
|
|
self.name_widget = w
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
|
|
|
self.widgets.append(QLabel('&'+self.col_metadata['name']+_(' index:'), parent))
|
|
w = QDoubleSpinBox(parent)
|
|
w.setRange(-100., float(100000000))
|
|
w.setDecimals(2)
|
|
w.setSingleStep(1)
|
|
self.idx_widget=w
|
|
self.widgets.append(w)
|
|
|
|
def initialize(self, book_id):
|
|
values = list(self.db.all_custom(num=self.col_id))
|
|
values.sort(key=sort_key)
|
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
|
s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True)
|
|
if s_index is None:
|
|
s_index = 0.0
|
|
self.idx_widget.setValue(s_index)
|
|
self.initial_index = s_index
|
|
self.initial_val = val
|
|
val = self.normalize_db_val(val)
|
|
idx = None
|
|
self.name_widget.clear()
|
|
for i, c in enumerate(values):
|
|
if c == val:
|
|
idx = i
|
|
self.name_widget.addItem(c)
|
|
self.name_widget.update_items_cache(values)
|
|
self.name_widget.setEditText('')
|
|
if idx is not None:
|
|
self.widgets[1].setCurrentIndex(idx)
|
|
|
|
def getter(self):
|
|
n = unicode(self.name_widget.currentText()).strip()
|
|
i = self.idx_widget.value()
|
|
return n, i
|
|
|
|
def commit(self, book_id, notify=False):
|
|
val, s_index = self.gui_val
|
|
val = self.normalize_ui_val(val)
|
|
if val != self.initial_val or s_index != self.initial_index:
|
|
if val == '':
|
|
val = s_index = None
|
|
elif s_index == 0.0:
|
|
if tweaks['series_index_auto_increment'] != 'const':
|
|
s_index = self.db.get_next_cc_series_num_for(val,
|
|
num=self.col_id)
|
|
else:
|
|
s_index = None
|
|
return self.db.set_custom(book_id, val, extra=s_index, num=self.col_id,
|
|
notify=notify, commit=False, allow_case_change=True)
|
|
else:
|
|
return set()
|
|
|
|
class Enumeration(Base):
|
|
|
|
def setup_ui(self, parent):
|
|
self.parent = parent
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
QComboBox(parent)]
|
|
w = self.widgets[1]
|
|
vals = self.col_metadata['display']['enum_values']
|
|
w.addItem('')
|
|
for v in vals:
|
|
w.addItem(v)
|
|
|
|
def initialize(self, book_id):
|
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
|
val = self.normalize_db_val(val)
|
|
self.initial_val = val
|
|
idx = self.widgets[1].findText(val)
|
|
if idx < 0:
|
|
error_dialog(self.parent, '',
|
|
_('The enumeration "{0}" contains an invalid value '
|
|
'that will be set to the default').format(
|
|
self.col_metadata['name']),
|
|
show=True, show_copy_button=False)
|
|
|
|
idx = 0
|
|
self.widgets[1].setCurrentIndex(idx)
|
|
|
|
def setter(self, val):
|
|
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
|
|
|
|
def getter(self):
|
|
return unicode(self.widgets[1].currentText())
|
|
|
|
def normalize_db_val(self, val):
|
|
if val is None:
|
|
val = ''
|
|
return val
|
|
|
|
def normalize_ui_val(self, val):
|
|
if not val:
|
|
val = None
|
|
return val
|
|
|
|
widgets = {
|
|
'bool' : Bool,
|
|
'rating' : Rating,
|
|
'int': Int,
|
|
'float': Float,
|
|
'datetime': DateTime,
|
|
'text' : Text,
|
|
'comments': Comments,
|
|
'series': Series,
|
|
'enumeration': Enumeration
|
|
}
|
|
|
|
def field_sort_key(y, fm=None):
|
|
m1 = fm[y]
|
|
name = icu_lower(m1['name'])
|
|
n1 = 'zzzzz' + name if m1['datatype'] == 'comments' else name
|
|
return sort_key(n1)
|
|
|
|
def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None):
|
|
def widget_factory(typ, key):
|
|
if bulk:
|
|
w = bulk_widgets[typ](db, key, parent)
|
|
else:
|
|
w = widgets[typ](db, key, parent)
|
|
if book_id is not None:
|
|
w.initialize(book_id)
|
|
return w
|
|
fm = db.field_metadata
|
|
|
|
# Get list of all non-composite custom fields. We must make widgets for these
|
|
fields = fm.custom_field_keys(include_composites=False)
|
|
cols_to_display = fields
|
|
cols_to_display.sort(key=partial(field_sort_key, fm=fm))
|
|
|
|
# This will contain the fields in the order to display them
|
|
cols = []
|
|
|
|
# The fields named here must be first in the widget list
|
|
tweak_cols = tweaks['metadata_edit_custom_column_order']
|
|
comments_in_tweak = 0
|
|
for key in (tweak_cols or ()):
|
|
# Add the key if it really exists in the database
|
|
if key in cols_to_display:
|
|
cols.append(key)
|
|
if fm[key]['datatype'] == 'comments':
|
|
comments_in_tweak += 1
|
|
|
|
# Add all the remaining fields
|
|
comments_not_in_tweak = 0
|
|
for key in cols_to_display:
|
|
if key not in cols:
|
|
cols.append(key)
|
|
if fm[key]['datatype'] == 'comments':
|
|
comments_not_in_tweak += 1
|
|
|
|
count = len(cols)
|
|
layout_rows_for_comments = 9
|
|
if two_column:
|
|
turnover_point = ((count-comments_not_in_tweak+1) +
|
|
comments_in_tweak*(layout_rows_for_comments-1))/2
|
|
else:
|
|
# Avoid problems with multi-line widgets
|
|
turnover_point = count + 1000
|
|
ans = []
|
|
column = row = base_row = max_row = 0
|
|
for key in cols:
|
|
if not fm[key]['is_editable']:
|
|
continue # this almost never happens
|
|
dt = fm[key]['datatype']
|
|
if dt == 'composite' or (bulk and dt == 'comments'):
|
|
continue
|
|
w = widget_factory(dt, fm[key]['colnum'])
|
|
ans.append(w)
|
|
if two_column and dt == 'comments':
|
|
# Here for compatibility with old layout. Comments always started
|
|
# in the left column
|
|
comments_in_tweak -= 1
|
|
# no special processing if the comment field was named in the tweak
|
|
if comments_in_tweak < 0 and comments_not_in_tweak > 0:
|
|
# Force a turnover, adding comments widgets below max_row.
|
|
# Save the row to return to if we turn over again
|
|
column = 0
|
|
row = max_row
|
|
base_row = row
|
|
turnover_point = row + (comments_not_in_tweak * layout_rows_for_comments)/2
|
|
comments_not_in_tweak = 0
|
|
|
|
l = QGridLayout()
|
|
if dt == 'comments':
|
|
layout.addLayout(l, row, column, layout_rows_for_comments, 1)
|
|
layout.setColumnStretch(column, 100)
|
|
row += layout_rows_for_comments
|
|
else:
|
|
layout.addLayout(l, row, column, 1, 1)
|
|
layout.setColumnStretch(column, 100)
|
|
row += 1
|
|
for c in range(0, len(w.widgets), 2):
|
|
if dt != 'comments':
|
|
w.widgets[c].setWordWrap(True)
|
|
w.widgets[c].setBuddy(w.widgets[c+1])
|
|
l.addWidget(w.widgets[c], c, 0)
|
|
l.addWidget(w.widgets[c+1], c, 1)
|
|
l.setColumnStretch(1, 10000)
|
|
else:
|
|
l.addWidget(w.widgets[0], 0, 0, 1, 2)
|
|
l.addItem(QSpacerItem(0, 0, vPolicy=QSizePolicy.Expanding), c, 0, 1, 1)
|
|
max_row = max(max_row, row)
|
|
if row >= turnover_point:
|
|
column = 1
|
|
turnover_point = count + 1000
|
|
row = base_row
|
|
|
|
items = []
|
|
if len(ans) > 0:
|
|
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
|
QSizePolicy.Expanding))
|
|
layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
|
|
layout.setRowStretch(layout.rowCount()-1, 100)
|
|
return ans, items
|
|
|
|
class BulkBase(Base):
|
|
|
|
@property
|
|
def gui_val(self):
|
|
if not hasattr(self, '_cached_gui_val_'):
|
|
self._cached_gui_val_ = self.getter()
|
|
return self._cached_gui_val_
|
|
|
|
|
|
def get_initial_value(self, book_ids):
|
|
values = set([])
|
|
for book_id in book_ids:
|
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
|
if isinstance(val, list):
|
|
val = frozenset(val)
|
|
values.add(val)
|
|
if len(values) > 1:
|
|
break
|
|
ans = None
|
|
if len(values) == 1:
|
|
ans = iter(values).next()
|
|
if isinstance(ans, frozenset):
|
|
ans = list(ans)
|
|
return ans
|
|
|
|
def initialize(self, book_ids):
|
|
self.initial_val = val = self.get_initial_value(book_ids)
|
|
val = self.normalize_db_val(val)
|
|
self.setter(val)
|
|
|
|
def commit(self, book_ids, notify=False):
|
|
if not self.a_c_checkbox.isChecked():
|
|
return
|
|
val = self.gui_val
|
|
val = self.normalize_ui_val(val)
|
|
if val != self.initial_val:
|
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
|
|
|
def make_widgets(self, parent, main_widget_class, extra_label_text=''):
|
|
w = QWidget(parent)
|
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', w), w]
|
|
l = QHBoxLayout()
|
|
l.setContentsMargins(0, 0, 0, 0)
|
|
w.setLayout(l)
|
|
self.main_widget = main_widget_class(w)
|
|
l.addWidget(self.main_widget)
|
|
l.setStretchFactor(self.main_widget, 10)
|
|
self.a_c_checkbox = QCheckBox( _('Apply changes'), w)
|
|
l.addWidget(self.a_c_checkbox)
|
|
self.ignore_change_signals = True
|
|
|
|
# connect to the various changed signals so we can auto-update the
|
|
# apply changes checkbox
|
|
if hasattr(self.main_widget, 'editTextChanged'):
|
|
# editable combobox widgets
|
|
self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed)
|
|
if hasattr(self.main_widget, 'textChanged'):
|
|
# lineEdit widgets
|
|
self.main_widget.textChanged.connect(self.a_c_checkbox_changed)
|
|
if hasattr(self.main_widget, 'currentIndexChanged'):
|
|
# combobox widgets
|
|
self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed)
|
|
if hasattr(self.main_widget, 'valueChanged'):
|
|
# spinbox widgets
|
|
self.main_widget.valueChanged.connect(self.a_c_checkbox_changed)
|
|
if hasattr(self.main_widget, 'dateTimeChanged'):
|
|
# dateEdit widgets
|
|
self.main_widget.dateTimeChanged.connect(self.a_c_checkbox_changed)
|
|
|
|
def a_c_checkbox_changed(self):
|
|
if not self.ignore_change_signals:
|
|
self.a_c_checkbox.setChecked(True)
|
|
|
|
class BulkBool(BulkBase, Bool):
|
|
|
|
def get_initial_value(self, book_ids):
|
|
value = None
|
|
for book_id in book_ids:
|
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
|
if not self.db.prefs.get('bools_are_tristate') and val is None:
|
|
val = False
|
|
if value is not None and value != val:
|
|
return None
|
|
value = val
|
|
return value
|
|
|
|
def setup_ui(self, parent):
|
|
self.make_widgets(parent, QComboBox)
|
|
items = [_('Yes'), _('No')]
|
|
if not self.db.prefs.get('bools_are_tristate'):
|
|
items.append('')
|
|
else:
|
|
items.append(_('Undefined'))
|
|
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
|
self.main_widget.blockSignals(True)
|
|
for icon, text in zip(icons, items):
|
|
self.main_widget.addItem(QIcon(icon), text)
|
|
self.main_widget.blockSignals(False)
|
|
|
|
def getter(self):
|
|
val = self.main_widget.currentIndex()
|
|
if not self.db.prefs.get('bools_are_tristate'):
|
|
return {2: False, 1: False, 0: True}[val]
|
|
else:
|
|
return {2: None, 1: False, 0: True}[val]
|
|
|
|
def setter(self, val):
|
|
val = {None: 2, False: 1, True: 0}[val]
|
|
self.main_widget.setCurrentIndex(val)
|
|
self.ignore_change_signals = False
|
|
|
|
def commit(self, book_ids, notify=False):
|
|
if not self.a_c_checkbox.isChecked():
|
|
return
|
|
val = self.gui_val
|
|
val = self.normalize_ui_val(val)
|
|
if not self.db.prefs.get('bools_are_tristate') and val is None:
|
|
val = False
|
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
|
|
|
def a_c_checkbox_changed(self):
|
|
if not self.ignore_change_signals:
|
|
if not self.db.prefs.get('bools_are_tristate') and \
|
|
self.main_widget.currentIndex() == 2:
|
|
self.a_c_checkbox.setChecked(False)
|
|
else:
|
|
self.a_c_checkbox.setChecked(True)
|
|
|
|
class BulkInt(BulkBase):
|
|
|
|
def setup_ui(self, parent):
|
|
self.make_widgets(parent, QSpinBox)
|
|
self.main_widget.setRange(-1000000, 100000000)
|
|
self.main_widget.setSpecialValueText(_('Undefined'))
|
|
self.main_widget.setSingleStep(1)
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
val = self.main_widget.minimum()
|
|
else:
|
|
val = int(val)
|
|
self.main_widget.setValue(val)
|
|
self.ignore_change_signals = False
|
|
|
|
def getter(self):
|
|
val = self.main_widget.value()
|
|
if val == self.main_widget.minimum():
|
|
val = None
|
|
return val
|
|
|
|
class BulkFloat(BulkInt):
|
|
|
|
def setup_ui(self, parent):
|
|
self.make_widgets(parent, QDoubleSpinBox)
|
|
self.main_widget.setRange(-1000000., float(100000000))
|
|
self.main_widget.setDecimals(2)
|
|
self.main_widget.setSpecialValueText(_('Undefined'))
|
|
self.main_widget.setSingleStep(1)
|
|
|
|
class BulkRating(BulkBase):
|
|
|
|
def setup_ui(self, parent):
|
|
self.make_widgets(parent, QSpinBox)
|
|
self.main_widget.setRange(0, 5)
|
|
self.main_widget.setSuffix(' '+_('star(s)'))
|
|
self.main_widget.setSpecialValueText(_('Unrated'))
|
|
self.main_widget.setSingleStep(1)
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
val = 0
|
|
self.main_widget.setValue(int(round(val/2.)))
|
|
self.ignore_change_signals = False
|
|
|
|
def getter(self):
|
|
val = self.main_widget.value()
|
|
if val == 0:
|
|
val = None
|
|
else:
|
|
val *= 2
|
|
return val
|
|
|
|
class BulkDateTime(BulkBase):
|
|
|
|
def setup_ui(self, parent):
|
|
cm = self.col_metadata
|
|
self.make_widgets(parent, DateTimeEdit)
|
|
self.widgets.append(QLabel(''))
|
|
w = QWidget(parent)
|
|
self.widgets.append(w)
|
|
l = QHBoxLayout()
|
|
l.setContentsMargins(0, 0, 0, 0)
|
|
w.setLayout(l)
|
|
l.addStretch(1)
|
|
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
|
|
l.addWidget(self.today_button)
|
|
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
|
|
l.addWidget(self.clear_button)
|
|
l.addStretch(2)
|
|
|
|
w = self.main_widget
|
|
format = cm['display'].get('date_format','')
|
|
if not format:
|
|
format = 'dd MMM yyyy'
|
|
w.setDisplayFormat(format)
|
|
w.setCalendarPopup(True)
|
|
w.setMinimumDateTime(UNDEFINED_QDATETIME)
|
|
w.setSpecialValueText(_('Undefined'))
|
|
self.today_button.clicked.connect(w.set_to_today)
|
|
self.clear_button.clicked.connect(w.set_to_clear)
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
val = self.main_widget.minimumDateTime()
|
|
else:
|
|
val = QDateTime(val)
|
|
self.main_widget.setDateTime(val)
|
|
self.ignore_change_signals = False
|
|
|
|
def getter(self):
|
|
val = self.main_widget.dateTime()
|
|
if val <= UNDEFINED_QDATETIME:
|
|
val = None
|
|
else:
|
|
val = qt_to_dt(val)
|
|
return val
|
|
|
|
class BulkSeries(BulkBase):
|
|
|
|
def setup_ui(self, parent):
|
|
self.make_widgets(parent, MultiCompleteComboBox)
|
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
|
values.sort(key=sort_key)
|
|
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
|
self.main_widget.setMinimumContentsLength(25)
|
|
self.widgets.append(QLabel('', parent))
|
|
w = QWidget(parent)
|
|
layout = QHBoxLayout(w)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
self.remove_series = QCheckBox(parent)
|
|
self.remove_series.setText(_('Remove series'))
|
|
layout.addWidget(self.remove_series)
|
|
self.idx_widget = QCheckBox(parent)
|
|
self.idx_widget.setText(_('Automatically number books'))
|
|
layout.addWidget(self.idx_widget)
|
|
self.force_number = QCheckBox(parent)
|
|
self.force_number.setText(_('Force numbers to start with '))
|
|
layout.addWidget(self.force_number)
|
|
self.series_start_number = QSpinBox(parent)
|
|
self.series_start_number.setMinimum(1)
|
|
self.series_start_number.setMaximum(9999999)
|
|
self.series_start_number.setProperty("value", 1)
|
|
layout.addWidget(self.series_start_number)
|
|
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
self.widgets.append(w)
|
|
self.idx_widget.stateChanged.connect(self.check_changed_checkbox)
|
|
self.force_number.stateChanged.connect(self.check_changed_checkbox)
|
|
self.series_start_number.valueChanged.connect(self.check_changed_checkbox)
|
|
self.remove_series.stateChanged.connect(self.check_changed_checkbox)
|
|
self.ignore_change_signals = False
|
|
|
|
def check_changed_checkbox(self):
|
|
self.a_c_checkbox.setChecked(True)
|
|
|
|
def initialize(self, book_id):
|
|
self.idx_widget.setChecked(False)
|
|
self.main_widget.set_separator(None)
|
|
self.main_widget.update_items_cache(self.all_values)
|
|
for c in self.all_values:
|
|
self.main_widget.addItem(c)
|
|
self.main_widget.setEditText('')
|
|
self.a_c_checkbox.setChecked(False)
|
|
|
|
def getter(self):
|
|
n = unicode(self.main_widget.currentText()).strip()
|
|
i = self.idx_widget.checkState()
|
|
f = self.force_number.checkState()
|
|
s = self.series_start_number.value()
|
|
r = self.remove_series.checkState()
|
|
return n, i, f, s, r
|
|
|
|
def commit(self, book_ids, notify=False):
|
|
if not self.a_c_checkbox.isChecked():
|
|
return
|
|
val, update_indices, force_start, at_value, clear = self.gui_val
|
|
val = None if clear else self.normalize_ui_val(val)
|
|
if clear or val != '':
|
|
extras = []
|
|
for book_id in book_ids:
|
|
if clear:
|
|
extras.append(None)
|
|
continue
|
|
if update_indices:
|
|
if force_start:
|
|
s_index = at_value
|
|
at_value += 1
|
|
elif tweaks['series_index_auto_increment'] != 'const':
|
|
s_index = self.db.get_next_cc_series_num_for(val, num=self.col_id)
|
|
else:
|
|
s_index = 1.0
|
|
else:
|
|
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
|
|
index_is_id=True)
|
|
extras.append(s_index)
|
|
self.db.set_custom_bulk(book_ids, val, extras=extras,
|
|
num=self.col_id, notify=notify)
|
|
|
|
class BulkEnumeration(BulkBase, Enumeration):
|
|
|
|
def get_initial_value(self, book_ids):
|
|
value = None
|
|
first = True
|
|
dialog_shown = False
|
|
for book_id in book_ids:
|
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
|
if val and val not in self.col_metadata['display']['enum_values']:
|
|
if not dialog_shown:
|
|
error_dialog(self.parent, '',
|
|
_('The enumeration "{0}" contains invalid values '
|
|
'that will not appear in the list').format(
|
|
self.col_metadata['name']),
|
|
show=True, show_copy_button=False)
|
|
dialog_shown = True
|
|
if first:
|
|
value = val
|
|
first = False
|
|
elif value != val:
|
|
value = None
|
|
if not value:
|
|
self.ignore_change_signals = False
|
|
return value
|
|
|
|
def setup_ui(self, parent):
|
|
self.parent = parent
|
|
self.make_widgets(parent, QComboBox)
|
|
vals = self.col_metadata['display']['enum_values']
|
|
self.main_widget.blockSignals(True)
|
|
self.main_widget.addItem('')
|
|
self.main_widget.addItems(vals)
|
|
self.main_widget.blockSignals(False)
|
|
|
|
def getter(self):
|
|
return unicode(self.main_widget.currentText())
|
|
|
|
def setter(self, val):
|
|
if val is None:
|
|
self.main_widget.setCurrentIndex(0)
|
|
else:
|
|
self.main_widget.setCurrentIndex(self.main_widget.findText(val))
|
|
self.ignore_change_signals = False
|
|
|
|
class RemoveTags(QWidget):
|
|
|
|
def __init__(self, parent, values):
|
|
QWidget.__init__(self, parent)
|
|
layout = QHBoxLayout()
|
|
layout.setSpacing(5)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.tags_box = MultiCompleteLineEdit(parent)
|
|
self.tags_box.update_items_cache(values)
|
|
layout.addWidget(self.tags_box, stretch=3)
|
|
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
|
layout.addWidget(self.checkbox)
|
|
layout.addStretch(1)
|
|
self.setLayout(layout)
|
|
self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched)
|
|
|
|
def box_touched(self, state):
|
|
if state:
|
|
self.tags_box.setText('')
|
|
self.tags_box.setEnabled(False)
|
|
else:
|
|
self.tags_box.setEnabled(True)
|
|
|
|
class BulkText(BulkBase):
|
|
|
|
def setup_ui(self, parent):
|
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
|
values.sort(key=sort_key)
|
|
if self.col_metadata['is_multiple']:
|
|
self.make_widgets(parent, MultiCompleteLineEdit,
|
|
extra_label_text=_('tags to add'))
|
|
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
|
self.adding_widget = self.main_widget
|
|
|
|
if not self.col_metadata['display'].get('is_names', False):
|
|
w = RemoveTags(parent, values)
|
|
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
|
_('tags to remove'), parent))
|
|
self.widgets.append(w)
|
|
self.removing_widget = w
|
|
self.main_widget.set_separator(',')
|
|
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
|
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
|
else:
|
|
self.main_widget.set_separator('&')
|
|
self.main_widget.set_space_before_sep(True)
|
|
self.main_widget.set_add_separator(
|
|
tweaks['authors_completer_append_separator'])
|
|
else:
|
|
self.make_widgets(parent, MultiCompleteComboBox)
|
|
self.main_widget.set_separator(None)
|
|
self.main_widget.setSizeAdjustPolicy(
|
|
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
|
self.main_widget.setMinimumContentsLength(25)
|
|
self.ignore_change_signals = False
|
|
|
|
def initialize(self, book_ids):
|
|
self.main_widget.update_items_cache(self.all_values)
|
|
if not self.col_metadata['is_multiple']:
|
|
val = self.get_initial_value(book_ids)
|
|
self.initial_val = val = self.normalize_db_val(val)
|
|
idx = None
|
|
self.main_widget.blockSignals(True)
|
|
for i, c in enumerate(self.all_values):
|
|
if c == val:
|
|
idx = i
|
|
self.main_widget.addItem(c)
|
|
self.main_widget.setEditText('')
|
|
if idx is not None:
|
|
self.main_widget.setCurrentIndex(idx)
|
|
self.main_widget.blockSignals(False)
|
|
|
|
def commit(self, book_ids, notify=False):
|
|
if not self.a_c_checkbox.isChecked():
|
|
return
|
|
if self.col_metadata['is_multiple']:
|
|
ism = self.col_metadata['multiple_seps']
|
|
if self.col_metadata['display'].get('is_names', False):
|
|
val = self.gui_val
|
|
add = [v.strip() for v in val.split(ism['ui_to_list']) if v.strip()]
|
|
self.db.set_custom_bulk(book_ids, add, num=self.col_id)
|
|
else:
|
|
remove_all, adding, rtext = self.gui_val
|
|
remove = set()
|
|
if remove_all:
|
|
remove = set(self.db.all_custom(num=self.col_id))
|
|
else:
|
|
txt = rtext
|
|
if txt:
|
|
remove = set([v.strip() for v in txt.split(ism['ui_to_list'])])
|
|
txt = adding
|
|
if txt:
|
|
add = set([v.strip() for v in txt.split(ism['ui_to_list'])])
|
|
else:
|
|
add = set()
|
|
self.db.set_custom_bulk_multiple(book_ids, add=add,
|
|
remove=remove, num=self.col_id)
|
|
else:
|
|
val = self.gui_val
|
|
val = self.normalize_ui_val(val)
|
|
if val != self.initial_val:
|
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
|
|
|
def getter(self):
|
|
if self.col_metadata['is_multiple']:
|
|
if not self.col_metadata['display'].get('is_names', False):
|
|
return self.removing_widget.checkbox.isChecked(), \
|
|
unicode(self.adding_widget.text()), \
|
|
unicode(self.removing_widget.tags_box.text())
|
|
return unicode(self.adding_widget.text())
|
|
val = unicode(self.main_widget.currentText()).strip()
|
|
if not val:
|
|
val = None
|
|
return val
|
|
|
|
|
|
bulk_widgets = {
|
|
'bool' : BulkBool,
|
|
'rating' : BulkRating,
|
|
'int': BulkInt,
|
|
'float': BulkFloat,
|
|
'datetime': BulkDateTime,
|
|
'text' : BulkText,
|
|
'series': BulkSeries,
|
|
'enumeration': BulkEnumeration,
|
|
}
|