diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index c873d1ed94..360a5bcd0a 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -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 diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index d7b9c62ed1..677ebac083 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -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))) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 67529397b3..dd4509acea 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -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 diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e1a1adc4ff..da1797bff8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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: