Bulk metadata edit: Custom column widgets all have an apply checkbox next to them.

This commit is contained in:
Kovid Goyal 2011-01-27 09:08:38 -07:00
commit 799ed2087d
4 changed files with 270 additions and 113 deletions

View File

@ -151,12 +151,27 @@ class DateEdit(QDateEdit):
def set_to_today(self):
self.setDate(now())
def set_to_clear(self):
self.setDate(UNDEFINED_QDATE)
class DateTime(Base):
def setup_ui(self, parent):
cm = self.col_metadata
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent),
QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)]
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(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:
@ -165,7 +180,8 @@ class DateTime(Base):
w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_QDATE)
w.setSpecialValueText(_('Undefined'))
self.widgets[3].clicked.connect(w.set_to_today)
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:
@ -470,11 +486,48 @@ class BulkBase(Base):
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, 'dateChanged'):
# dateEdit widgets
self.main_widget.dateChanged.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):
@ -484,58 +537,144 @@ class BulkBool(BulkBase, Bool):
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
val = False
if value is not None and value != val:
return 'nochange'
return None
value = val
return value
def setup_ui(self, parent):
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')]
icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')]
self.make_widgets(parent, QComboBox)
items = [_('Yes'), _('No'), _('Undefined')]
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
self.main_widget.blockSignals(True)
for icon, text in zip(icons, items):
w.addItem(QIcon(icon), text)
self.main_widget.addItem(QIcon(icon), text)
self.main_widget.blockSignals(False)
def getter(self):
val = self.widgets[1].currentIndex()
return {3: 'nochange', 2: None, 1: False, 0: True}[val]
val = self.main_widget.currentIndex()
return {2: None, 1: False, 0: True}[val]
def setter(self, val):
val = {'nochange': 3, None: 2, False: 1, True: 0}[val]
self.widgets[1].setCurrentIndex(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 val != self.initial_val and val != 'nochange':
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
val = False
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
val = False
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
class BulkInt(BulkBase, Int):
pass
class BulkInt(BulkBase):
class BulkFloat(BulkBase, Float):
pass
def setup_ui(self, parent):
self.make_widgets(parent, QSpinBox)
self.main_widget.setRange(-100, sys.maxint)
self.main_widget.setSpecialValueText(_('Undefined'))
self.main_widget.setSingleStep(1)
class BulkRating(BulkBase, Rating):
pass
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
class BulkDateTime(BulkBase, DateTime):
pass
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(-100., float(sys.maxint))
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, DateEdit)
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.setMinimumDate(UNDEFINED_QDATE)
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.minimumDate()
else:
val = QDate(val.year, val.month, val.day)
self.main_widget.setDate(val)
self.ignore_change_signals = False
def getter(self):
val = self.main_widget.date()
if val == UNDEFINED_QDATE:
val = None
else:
val = qt_to_dt(val)
return val
class BulkSeries(BulkBase):
def setup_ui(self, parent):
self.make_widgets(parent, EnComboBox)
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
w = EnComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
self.name_widget = w
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.main_widget.setMinimumContentsLength(25)
self.widgets.append(QLabel('', parent))
w = QWidget(parent)
layout = QHBoxLayout(w)
@ -555,15 +694,24 @@ class BulkSeries(BulkBase):
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)
for c in self.all_values:
self.name_widget.addItem(c)
self.name_widget.setEditText('')
self.main_widget.addItem(c)
self.main_widget.setEditText('')
self.a_c_checkbox.setChecked(False)
def getter(self):
n = unicode(self.name_widget.currentText()).strip()
n = unicode(self.main_widget.currentText()).strip()
i = self.idx_widget.checkState()
f = self.force_number.checkState()
s = self.series_start_number.value()
@ -571,6 +719,8 @@ class BulkSeries(BulkBase):
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 != '':
@ -598,9 +748,9 @@ class BulkEnumeration(BulkBase, Enumeration):
def get_initial_value(self, book_ids):
value = None
ret_value = None
first = True
dialog_shown = False
for i,book_id in enumerate(book_ids):
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:
@ -610,44 +760,32 @@ class BulkEnumeration(BulkBase, Enumeration):
self.col_metadata['name']),
show=True, show_copy_button=False)
dialog_shown = True
ret_value = ' nochange '
elif (value is not None and value != val) or (val and i != 0):
ret_value = ' nochange '
value = val
if ret_value is None:
return value
return ret_value
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.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
self.make_widgets(parent, QComboBox)
vals = self.col_metadata['display']['enum_values']
w.addItem('Do Not Change')
w.addItem('')
for v in vals:
w.addItem(v)
self.main_widget.blockSignals(True)
self.main_widget.addItem('')
self.main_widget.addItems(vals)
self.main_widget.blockSignals(False)
def getter(self):
if self.widgets[1].currentIndex() == 0:
return ' nochange '
return unicode(self.widgets[1].currentText())
return unicode(self.main_widget.currentText())
def setter(self, val):
if val == ' nochange ':
self.widgets[1].setCurrentIndex(0)
if val is None:
self.main_widget.setCurrentIndex(0)
else:
if val is None:
self.widgets[1].setCurrentIndex(1)
else:
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
def commit(self, book_ids, notify=False):
val = self.gui_val
val = self.normalize_ui_val(val)
if val != self.initial_val and val != ' nochange ':
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
self.main_widget.setCurrentIndex(self.main_widget.findText(val))
self.ignore_change_signals = False
class RemoveTags(QWidget):
@ -658,11 +796,10 @@ class RemoveTags(QWidget):
layout.setContentsMargins(0, 0, 0, 0)
self.tags_box = CompleteLineEdit(parent, values)
layout.addWidget(self.tags_box, stretch = 1)
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
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)
@ -679,39 +816,45 @@ class BulkText(BulkBase):
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
if self.col_metadata['is_multiple']:
w = CompleteLineEdit(parent, values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' +
_('tags to add'), parent), w]
self.adding_widget = w
self.make_widgets(parent, CompleteLineEdit,
extra_label_text=_('tags to add'))
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.adding_widget = self.main_widget
w = RemoveTags(parent, values)
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
_('tags to remove'), parent))
self.widgets.append(w)
self.removing_widget = w
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
else:
w = EnComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
self.make_widgets(parent, EnComboBox)
self.main_widget.setSizeAdjustPolicy(
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.main_widget.setMinimumContentsLength(25)
self.ignore_change_signals = False
def initialize(self, book_ids):
if self.col_metadata['is_multiple']:
self.widgets[1].update_items_cache(self.all_values)
self.main_widget.update_items_cache(self.all_values)
else:
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.widgets[1].addItem(c)
self.widgets[1].setEditText('')
self.main_widget.addItem(c)
self.main_widget.setEditText('')
if idx is not None:
self.widgets[1].setCurrentIndex(idx)
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']:
remove_all, adding, rtext = self.gui_val
remove = set()
@ -740,7 +883,7 @@ class BulkText(BulkBase):
unicode(self.adding_widget.text()), \
unicode(self.removing_widget.tags_box.text())
val = unicode(self.widgets[1].currentText()).strip()
val = unicode(self.main_widget.currentText()).strip()
if not val:
val = None
return val

View File

@ -64,6 +64,8 @@ class TagDelegate(QItemDelegate): # {{{
# }}}
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal()
@ -177,9 +179,16 @@ class TagsView(QTreeView): # {{{
return joiner.join(tokens)
def toggle(self, index):
self._toggle(index, None)
def _toggle(self, index, set_to):
'''
set_to: if None, advance the state. Otherwise must be one of the values
in TAG_SEARCH_STATES
'''
modifiers = int(QApplication.keyboardModifiers())
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
if self._model.toggle(index, exclusive):
if self._model.toggle(index, exclusive, set_to=set_to):
self.tags_marked.emit(self.search_string)
def conditional_clear(self, search_string):
@ -187,7 +196,7 @@ class TagsView(QTreeView): # {{{
self.clear()
def context_menu_handler(self, action=None, category=None,
key=None, index=None, negate=None):
key=None, index=None, search_state=None):
if not action:
return
try:
@ -201,11 +210,10 @@ class TagsView(QTreeView): # {{{
self.user_category_edit.emit(category)
return
if action == 'search':
self.tags_marked.emit(('not ' if negate else '') +
category + ':"=' + key + '"')
self._toggle(index, set_to=search_state)
return
if action == 'search_category':
self.tags_marked.emit(category + ':' + str(not negate))
self.tags_marked.emit(key + ':' + search_state)
return
if action == 'manage_searches':
self.saved_search_edit.emit(category)
@ -270,20 +278,16 @@ class TagsView(QTreeView): # {{{
partial(self.context_menu_handler,
action='edit_author_sort', index=tag_id))
# Add the search for value items
n = tag_name
c = category
if self.db.field_metadata[key]['datatype'] == 'rating':
n = str(len(tag_name))
elif self.db.field_metadata[key]['kind'] in ['user', 'search']:
c = tag_item.tag.category
self.context_menu.addAction(self.search_icon,
_('Search for %s')%tag_name,
partial(self.context_menu_handler, action='search',
category=c, key=n, negate=False))
search_state=TAG_SEARCH_STATES['mark_plus'],
index=index))
self.context_menu.addAction(self.search_icon,
_('Search for everything but %s')%tag_name,
partial(self.context_menu_handler, action='search',
category=c, key=n, negate=True))
search_state=TAG_SEARCH_STATES['mark_minus'],
index=index))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category,
@ -299,11 +303,11 @@ class TagsView(QTreeView): # {{{
self.context_menu.addAction(self.search_icon,
_('Search for books in category %s')%category,
partial(self.context_menu_handler, action='search_category',
category=key, negate=False))
key=key, search_state='true'))
self.context_menu.addAction(self.search_icon,
_('Search for books not in category %s')%category,
partial(self.context_menu_handler, action='search_category',
category=key, negate=True))
key=key, search_state='false'))
# Offer specific editors for tags/series/publishers/saved searches
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \
@ -528,9 +532,15 @@ class TagTreeItem(object): # {{{
return QVariant(self.tooltip)
return NONE
def toggle(self):
def toggle(self, set_to=None):
'''
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
'''
if self.type == self.TAG:
self.tag.state = (self.tag.state + 1)%3
if set_to is None:
self.tag.state = (self.tag.state + 1)%3
else:
self.tag.state = set_to
def child_tags(self):
res = []
@ -1014,11 +1024,15 @@ class TagsModel(QAbstractItemModel): # {{{
def clear_state(self):
self.reset_all_states()
def toggle(self, index, exclusive):
def toggle(self, index, exclusive, set_to=None):
'''
exclusive: clear all states before applying this one
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
'''
if not index.isValid(): return False
item = index.internalPointer()
if item.type == TagTreeItem.TAG:
item.toggle()
item.toggle(set_to=set_to)
if exclusive:
self.reset_all_states(except_=item.tag)
self.dataChanged.emit(index, index)
@ -1040,8 +1054,9 @@ class TagsModel(QAbstractItemModel): # {{{
category_item = self.root_item.children[row_index]
for tag_item in category_item.child_tags():
tag = tag_item.tag
if tag.state > 0:
prefix = ' not ' if tag.state == 2 else ''
if tag.state != TAG_SEARCH_STATES['clear']:
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
else ''
category = key if key != 'news' else 'tag'
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))

View File

@ -49,7 +49,7 @@ class MetadataBackup(Thread): # {{{
def run(self):
while self.keep_running:
try:
time.sleep(2) # Limit to two per second
time.sleep(2) # Limit to one book per two seconds
(id_, sequence) = self.db.get_a_dirtied_book()
if id_ is None:
continue

View File

@ -618,9 +618,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''
with self.dirtied_lock:
dc_sequence = self.dirtied_cache.get(book_id, None)
# print 'clear_dirty: check book', book_id, dc_sequence
# print 'clear_dirty: check book', book_id, dc_sequence
if dc_sequence is None or sequence is None or dc_sequence == sequence:
# print 'needs to be cleaned'
# print 'needs to be cleaned'
self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?',
(book_id,))
self.conn.commit()
@ -629,7 +629,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except:
pass
elif dc_sequence is not None:
# print 'book needs to be done again'
# print 'book needs to be done again'
pass
def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
@ -661,12 +661,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
changed = False
for book in book_ids:
with self.dirtied_lock:
# print 'dirtied: check id', book
# print 'dirtied: check id', book
if book in self.dirtied_cache:
self.dirtied_cache[book] = self.dirtied_sequence
self.dirtied_sequence += 1
continue
# print 'book not already dirty'
# print 'book not already dirty'
try:
self.conn.execute(
'INSERT INTO metadata_dirtied (book) VALUES (?)',
@ -720,7 +720,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# thread has not done the work between the put and the get_metadata
with self.dirtied_lock:
sequence = self.dirtied_cache.get(idx, None)
# print 'get_md_for_dump', idx, sequence
# print 'get_md_for_dump', idx, sequence
try:
# While a book is being created, the path is empty. Don't bother to
# try to write the opf, because it will go to the wrong folder.
@ -827,7 +827,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
try:
book_ids = self.data.parse(query)
except:
import traceback
traceback.print_exc()
return identical_book_ids
for book_id in book_ids: