1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import os 10from functools import partial 11 12from qt.core import (Qt, QComboBox, QLabel, QSpinBox, QDoubleSpinBox, 13 QDateTime, QGroupBox, QVBoxLayout, QSizePolicy, QGridLayout, QUrl, 14 QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, QLineEdit, 15 QMessageBox, QToolButton, QPlainTextEdit, QApplication, QStyle, QDialog) 16 17from calibre.utils.date import qt_to_dt, now, as_local_time, as_utc, internal_iso_format_string 18from calibre.gui2.complete2 import EditWithComplete as EWC 19from calibre.gui2.comments_editor import Editor as CommentsEditor 20from calibre.gui2 import UNDEFINED_QDATETIME, error_dialog, elided_text 21from calibre.gui2.dialogs.tag_editor import TagEditor 22from calibre.utils.config import tweaks 23from calibre.utils.icu import sort_key 24from calibre.library.comments import comments_to_html 25from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox 26from calibre.gui2.widgets2 import RatingEditor, DateTimeEdit as DateTimeEditBase 27 28 29class EditWithComplete(EWC): 30 31 def __init__(self, *a, **kw): 32 super().__init__(*a, **kw) 33 self.set_clear_button_enabled(False) 34 35 36def safe_disconnect(signal): 37 try: 38 signal.disconnect() 39 except Exception: 40 pass 41 42 43def label_string(txt): 44 if txt: 45 try: 46 if txt[0].isalnum(): 47 return '&' + txt 48 except: 49 pass 50 return txt 51 52 53def get_tooltip(col_metadata, add_index=False): 54 key = col_metadata['label'] + ('_index' if add_index else '') 55 label = col_metadata['name'] + (_(' index') if add_index else '') 56 description = col_metadata.get('display', {}).get('description', '') 57 return '{} (#{}){} {}'.format( 58 label, key, ':' if description else '', description).strip() 59 60 61class Base: 62 63 def __init__(self, db, col_id, parent=None): 64 self.db, self.col_id = db, col_id 65 self.book_id = None 66 self.col_metadata = db.custom_column_num_map[col_id] 67 self.initial_val = self.widgets = None 68 self.signals_to_disconnect = [] 69 self.setup_ui(parent) 70 description = get_tooltip(self.col_metadata) 71 try: 72 self.widgets[0].setToolTip(description) 73 self.widgets[1].setToolTip(description) 74 except: 75 try: 76 self.widgets[1].setToolTip(description) 77 except: 78 pass 79 80 def finish_ui_setup(self, parent, edit_widget): 81 self.was_none = False 82 w = QWidget(parent) 83 self.widgets.append(w) 84 l = QHBoxLayout() 85 l.setContentsMargins(0, 0, 0, 0) 86 w.setLayout(l) 87 self.editor = editor = edit_widget(parent) 88 l.addWidget(editor) 89 self.clear_button = QToolButton(parent) 90 self.clear_button.setIcon(QIcon(I('trash.png'))) 91 self.clear_button.clicked.connect(self.set_to_undefined) 92 self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name'])) 93 l.addWidget(self.clear_button) 94 95 def initialize(self, book_id): 96 self.book_id = book_id 97 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 98 val = self.normalize_db_val(val) 99 self.setter(val) 100 self.initial_val = self.current_val # self.current_val might be different from val thanks to normalization 101 102 @property 103 def current_val(self): 104 return self.normalize_ui_val(self.gui_val) 105 106 @property 107 def gui_val(self): 108 return self.getter() 109 110 def commit(self, book_id, notify=False): 111 val = self.current_val 112 if val != self.initial_val: 113 return self.db.set_custom(book_id, val, num=self.col_id, 114 notify=notify, commit=False, allow_case_change=True) 115 else: 116 return set() 117 118 def apply_to_metadata(self, mi): 119 mi.set('#' + self.col_metadata['label'], self.current_val) 120 121 def normalize_db_val(self, val): 122 return val 123 124 def normalize_ui_val(self, val): 125 return val 126 127 def break_cycles(self): 128 self.db = self.widgets = self.initial_val = None 129 for signal in self.signals_to_disconnect: 130 safe_disconnect(signal) 131 self.signals_to_disconnect = [] 132 133 def connect_data_changed(self, slot): 134 pass 135 136 137class SimpleText(Base): 138 139 def setup_ui(self, parent): 140 self.editor = QLineEdit(parent) 141 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent), 142 self.editor] 143 self.editor.setClearButtonEnabled(True) 144 145 def setter(self, val): 146 self.editor.setText(str(val or '')) 147 148 def getter(self): 149 return self.editor.text().strip() 150 151 def connect_data_changed(self, slot): 152 self.editor.textChanged.connect(slot) 153 self.signals_to_disconnect.append(self.editor.textChanged) 154 155 156class LongText(Base): 157 158 def setup_ui(self, parent): 159 self._box = QGroupBox(parent) 160 self._box.setTitle(label_string(self.col_metadata['name'])) 161 self._layout = QVBoxLayout() 162 self._tb = QPlainTextEdit(self._box) 163 self._tb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 164 self._layout.addWidget(self._tb) 165 self._box.setLayout(self._layout) 166 self.widgets = [self._box] 167 168 def setter(self, val): 169 self._tb.setPlainText(str(val or '')) 170 171 def getter(self): 172 return self._tb.toPlainText() 173 174 def connect_data_changed(self, slot): 175 self._tb.textChanged.connect(slot) 176 self.signals_to_disconnect.append(self._tb.textChanged) 177 178 179class Bool(Base): 180 181 def setup_ui(self, parent): 182 name = self.col_metadata['name'] 183 self.widgets = [QLabel(label_string(name), parent)] 184 w = QWidget(parent) 185 self.widgets.append(w) 186 187 l = QHBoxLayout() 188 l.setContentsMargins(0, 0, 0, 0) 189 w.setLayout(l) 190 self.combobox = QComboBox(parent) 191 l.addWidget(self.combobox) 192 193 c = QToolButton(parent) 194 c.setIcon(QIcon(I('ok.png'))) 195 c.setToolTip(_('Set {} to yes').format(name)) 196 l.addWidget(c) 197 c.clicked.connect(self.set_to_yes) 198 199 c = QToolButton(parent) 200 c.setIcon(QIcon(I('list_remove.png'))) 201 c.setToolTip(_('Set {} to no').format(name)) 202 l.addWidget(c) 203 c.clicked.connect(self.set_to_no) 204 205 if self.db.new_api.pref('bools_are_tristate'): 206 c = QToolButton(parent) 207 c.setIcon(QIcon(I('trash.png'))) 208 c.setToolTip(_('Clear {}').format(name)) 209 l.addWidget(c) 210 c.clicked.connect(self.set_to_cleared) 211 212 w = self.combobox 213 items = [_('Yes'), _('No'), _('Undefined')] 214 icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] 215 if not self.db.new_api.pref('bools_are_tristate'): 216 items = items[:-1] 217 icons = icons[:-1] 218 for icon, text in zip(icons, items): 219 w.addItem(QIcon(icon), text) 220 221 def setter(self, val): 222 val = {None: 2, False: 1, True: 0}[val] 223 if not self.db.new_api.pref('bools_are_tristate') and val == 2: 224 val = 1 225 self.combobox.setCurrentIndex(val) 226 227 def getter(self): 228 val = self.combobox.currentIndex() 229 return {2: None, 1: False, 0: True}[val] 230 231 def set_to_yes(self): 232 self.combobox.setCurrentIndex(0) 233 234 def set_to_no(self): 235 self.combobox.setCurrentIndex(1) 236 237 def set_to_cleared(self): 238 self.combobox.setCurrentIndex(2) 239 240 def connect_data_changed(self, slot): 241 self.combobox.currentTextChanged.connect(slot) 242 self.signals_to_disconnect.append(self.combobox.currentTextChanged) 243 244 245class Int(Base): 246 247 def setup_ui(self, parent): 248 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 249 self.finish_ui_setup(parent, ClearingSpinBox) 250 self.editor.setRange(-1000000, 100000000) 251 252 def finish_ui_setup(self, parent, edit_widget): 253 Base.finish_ui_setup(self, parent, edit_widget) 254 self.editor.setSpecialValueText(_('Undefined')) 255 self.editor.setSingleStep(1) 256 self.editor.valueChanged.connect(self.valueChanged) 257 258 def setter(self, val): 259 if val is None: 260 val = self.editor.minimum() 261 self.editor.setValue(val) 262 self.was_none = val == self.editor.minimum() 263 264 def getter(self): 265 val = self.editor.value() 266 if val == self.editor.minimum(): 267 val = None 268 return val 269 270 def valueChanged(self, to_what): 271 if self.was_none and to_what == -999999: 272 self.setter(0) 273 self.was_none = to_what == self.editor.minimum() 274 275 def connect_data_changed(self, slot): 276 self.editor.valueChanged.connect(slot) 277 self.signals_to_disconnect.append(self.editor.valueChanged) 278 279 def set_to_undefined(self): 280 self.editor.setValue(-1000000) 281 282 283class Float(Int): 284 285 def setup_ui(self, parent): 286 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 287 self.finish_ui_setup(parent, ClearingDoubleSpinBox) 288 self.editor.setRange(-1000000., float(100000000)) 289 self.editor.setDecimals(2) 290 291 292class Rating(Base): 293 294 def setup_ui(self, parent): 295 allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False) 296 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 297 self.finish_ui_setup(parent, partial(RatingEditor, is_half_star=allow_half_stars)) 298 299 def set_to_undefined(self): 300 self.editor.setCurrentIndex(0) 301 302 def setter(self, val): 303 val = max(0, min(int(val or 0), 10)) 304 self.editor.rating_value = val 305 306 def getter(self): 307 return self.editor.rating_value or None 308 309 def connect_data_changed(self, slot): 310 self.editor.currentTextChanged.connect(slot) 311 self.signals_to_disconnect.append(self.editor.currentTextChanged) 312 313 314class DateTimeEdit(DateTimeEditBase): 315 316 def focusInEvent(self, x): 317 self.setSpecialValueText('') 318 DateTimeEditBase.focusInEvent(self, x) 319 320 def focusOutEvent(self, x): 321 self.setSpecialValueText(_('Undefined')) 322 DateTimeEditBase.focusOutEvent(self, x) 323 324 def set_to_today(self): 325 self.setDateTime(now()) 326 327 def set_to_clear(self): 328 self.setDateTime(now()) 329 self.setDateTime(UNDEFINED_QDATETIME) 330 331 332class DateTime(Base): 333 334 def setup_ui(self, parent): 335 cm = self.col_metadata 336 self.widgets = [QLabel(label_string(cm['name']), parent)] 337 w = QWidget(parent) 338 self.widgets.append(w) 339 l = QHBoxLayout() 340 l.setContentsMargins(0, 0, 0, 0) 341 w.setLayout(l) 342 self.dte = dte = DateTimeEdit(parent) 343 format_ = cm['display'].get('date_format','') 344 if not format_: 345 format_ = 'dd MMM yyyy hh:mm' 346 elif format_ == 'iso': 347 format_ = internal_iso_format_string() 348 dte.setDisplayFormat(format_) 349 dte.setCalendarPopup(True) 350 dte.setMinimumDateTime(UNDEFINED_QDATETIME) 351 dte.setSpecialValueText(_('Undefined')) 352 l.addWidget(dte) 353 354 self.today_button = QToolButton(parent) 355 self.today_button.setText(_('Today')) 356 self.today_button.clicked.connect(dte.set_to_today) 357 l.addWidget(self.today_button) 358 359 self.clear_button = QToolButton(parent) 360 self.clear_button.setIcon(QIcon(I('trash.png'))) 361 self.clear_button.clicked.connect(dte.set_to_clear) 362 self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name'])) 363 l.addWidget(self.clear_button) 364 365 def setter(self, val): 366 if val is None: 367 val = self.dte.minimumDateTime() 368 else: 369 val = QDateTime(val) 370 self.dte.setDateTime(val) 371 372 def getter(self): 373 val = self.dte.dateTime() 374 if val <= UNDEFINED_QDATETIME: 375 val = None 376 else: 377 val = qt_to_dt(val) 378 return val 379 380 def normalize_db_val(self, val): 381 return as_local_time(val) if val is not None else None 382 383 def normalize_ui_val(self, val): 384 return as_utc(val) if val is not None else None 385 386 def connect_data_changed(self, slot): 387 self.dte.dateTimeChanged.connect(slot) 388 self.signals_to_disconnect.append(self.dte.dateTimeChanged) 389 390 391class Comments(Base): 392 393 def setup_ui(self, parent): 394 self._box = QGroupBox(parent) 395 self._box.setTitle(label_string(self.col_metadata['name'])) 396 self._layout = QVBoxLayout() 397 self._tb = CommentsEditor(self._box, toolbar_prefs_name='metadata-comments-editor-widget-hidden-toolbars') 398 self._tb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 399 # self._tb.setTabChangesFocus(True) 400 self._layout.addWidget(self._tb) 401 self._box.setLayout(self._layout) 402 self.widgets = [self._box] 403 404 def initialize(self, book_id): 405 path = self.db.abspath(book_id, index_is_id=True) 406 if path: 407 self._tb.set_base_url(QUrl.fromLocalFile(os.path.join(path, 'metadata.html'))) 408 return Base.initialize(self, book_id) 409 410 def setter(self, val): 411 if not val or not val.strip(): 412 val = '' 413 else: 414 val = comments_to_html(val) 415 self._tb.html = val 416 self._tb.wyswyg_dirtied() 417 418 def getter(self): 419 val = str(self._tb.html).strip() 420 if not val: 421 val = None 422 return val 423 424 @property 425 def tab(self): 426 return self._tb.tab 427 428 @tab.setter 429 def tab(self, val): 430 self._tb.tab = val 431 432 def connect_data_changed(self, slot): 433 self._tb.data_changed.connect(slot) 434 self.signals_to_disconnect.append(self._tb.data_changed) 435 436 437class MultipleWidget(QWidget): 438 439 def __init__(self, parent): 440 QWidget.__init__(self, parent) 441 layout = QHBoxLayout() 442 layout.setSpacing(5) 443 layout.setContentsMargins(0, 0, 0, 0) 444 445 self.tags_box = EditWithComplete(parent) 446 layout.addWidget(self.tags_box, stretch=1000) 447 self.editor_button = QToolButton(self) 448 self.editor_button.setToolTip(_('Open Item editor. If CTRL or SHIFT is pressed, open Manage items')) 449 self.editor_button.setIcon(QIcon(I('chapters.png'))) 450 layout.addWidget(self.editor_button) 451 self.setLayout(layout) 452 453 def get_editor_button(self): 454 return self.editor_button 455 456 def update_items_cache(self, values): 457 self.tags_box.update_items_cache(values) 458 459 def clear(self): 460 self.tags_box.clear() 461 462 def setEditText(self): 463 self.tags_box.setEditText() 464 465 def addItem(self, itm): 466 self.tags_box.addItem(itm) 467 468 def set_separator(self, sep): 469 self.tags_box.set_separator(sep) 470 471 def set_add_separator(self, sep): 472 self.tags_box.set_add_separator(sep) 473 474 def set_space_before_sep(self, v): 475 self.tags_box.set_space_before_sep(v) 476 477 def setSizePolicy(self, v1, v2): 478 self.tags_box.setSizePolicy(v1, v2) 479 480 def setText(self, v): 481 self.tags_box.setText(v) 482 483 def text(self): 484 return self.tags_box.text() 485 486 487def _save_dialog(parent, title, msg, det_msg=''): 488 d = QMessageBox(parent) 489 d.setWindowTitle(title) 490 d.setText(msg) 491 d.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel) 492 return d.exec() 493 494 495class Text(Base): 496 497 def setup_ui(self, parent): 498 self.sep = self.col_metadata['multiple_seps'] 499 self.key = self.db.field_metadata.label_to_key(self.col_metadata['label'], 500 prefer_custom=True) 501 self.parent = parent 502 503 if self.col_metadata['is_multiple']: 504 w = MultipleWidget(parent) 505 w.set_separator(self.sep['ui_to_list']) 506 if self.sep['ui_to_list'] == '&': 507 w.set_space_before_sep(True) 508 w.set_add_separator(tweaks['authors_completer_append_separator']) 509 w.get_editor_button().clicked.connect(self.edit) 510 w.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) 511 self.set_to_undefined = w.clear 512 else: 513 w = EditWithComplete(parent) 514 w.set_separator(None) 515 w.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) 516 w.setMinimumContentsLength(25) 517 self.set_to_undefined = w.clearEditText 518 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 519 self.finish_ui_setup(parent, lambda parent: w) 520 521 def initialize(self, book_id): 522 values = list(self.db.all_custom(num=self.col_id)) 523 values.sort(key=sort_key) 524 self.book_id = book_id 525 self.editor.clear() 526 self.editor.update_items_cache(values) 527 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 528 if isinstance(val, list): 529 if not self.col_metadata.get('display', {}).get('is_names', False): 530 val.sort(key=sort_key) 531 val = self.normalize_db_val(val) 532 533 if self.col_metadata['is_multiple']: 534 self.setter(val) 535 else: 536 self.editor.setText(val) 537 self.initial_val = self.current_val 538 539 def setter(self, val): 540 if self.col_metadata['is_multiple']: 541 if not val: 542 val = [] 543 self.editor.setText(self.sep['list_to_ui'].join(val)) 544 545 def getter(self): 546 if self.col_metadata['is_multiple']: 547 val = str(self.editor.text()).strip() 548 ans = [x.strip() for x in val.split(self.sep['ui_to_list']) if x.strip()] 549 if not ans: 550 ans = None 551 return ans 552 val = str(self.editor.currentText()).strip() 553 if not val: 554 val = None 555 return val 556 557 def edit(self): 558 ctrl_or_shift_pressed = (QApplication.keyboardModifiers() & 559 (Qt.KeyboardModifier.ControlModifier + Qt.KeyboardModifier.ShiftModifier)) 560 if (self.getter() != self.initial_val and (self.getter() or self.initial_val)): 561 d = _save_dialog(self.parent, _('Values changed'), 562 _('You have changed the values. In order to use this ' 563 'editor, you must either discard or apply these ' 564 'changes. Apply changes?')) 565 if d == QMessageBox.StandardButton.Cancel: 566 return 567 if d == QMessageBox.StandardButton.Yes: 568 self.commit(self.book_id) 569 self.db.commit() 570 self.initial_val = self.current_val 571 else: 572 self.setter(self.initial_val) 573 if ctrl_or_shift_pressed: 574 from calibre.gui2.ui import get_gui 575 get_gui().do_tags_list_edit(None, self.key) 576 self.initialize(self.book_id) 577 else: 578 d = TagEditor(self.parent, self.db, self.book_id, self.key) 579 if d.exec() == QDialog.DialogCode.Accepted: 580 self.setter(d.tags) 581 582 def connect_data_changed(self, slot): 583 if self.col_metadata['is_multiple']: 584 s = self.editor.tags_box.currentTextChanged 585 else: 586 s = self.editor.currentTextChanged 587 s.connect(slot) 588 self.signals_to_disconnect.append(s) 589 590 591class Series(Base): 592 593 def setup_ui(self, parent): 594 w = EditWithComplete(parent) 595 w.set_separator(None) 596 w.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) 597 w.setMinimumContentsLength(25) 598 self.name_widget = w 599 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 600 self.finish_ui_setup(parent, lambda parent: w) 601 w.editTextChanged.connect(self.series_changed) 602 603 w = QLabel(label_string(self.col_metadata['name'])+_(' index'), parent) 604 w.setToolTip(get_tooltip(self.col_metadata, add_index=True)) 605 self.widgets.append(w) 606 w = QDoubleSpinBox(parent) 607 w.setRange(-10000., float(100000000)) 608 w.setDecimals(2) 609 w.setSingleStep(1) 610 self.idx_widget=w 611 w.setToolTip(get_tooltip(self.col_metadata, add_index=True)) 612 self.widgets.append(w) 613 614 def set_to_undefined(self): 615 self.name_widget.clearEditText() 616 self.idx_widget.setValue(1.0) 617 618 def initialize(self, book_id): 619 values = list(self.db.all_custom(num=self.col_id)) 620 values.sort(key=sort_key) 621 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 622 s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True) 623 try: 624 s_index = float(s_index) 625 except (ValueError, TypeError): 626 s_index = 1.0 627 self.idx_widget.setValue(s_index) 628 val = self.normalize_db_val(val) 629 self.name_widget.blockSignals(True) 630 self.name_widget.update_items_cache(values) 631 self.name_widget.setText(val) 632 self.name_widget.blockSignals(False) 633 self.initial_val, self.initial_index = self.current_val 634 635 def getter(self): 636 n = str(self.name_widget.currentText()).strip() 637 i = self.idx_widget.value() 638 return n, i 639 640 def series_changed(self, val): 641 val, s_index = self.gui_val 642 if tweaks['series_index_auto_increment'] == 'no_change': 643 pass 644 elif tweaks['series_index_auto_increment'] == 'const': 645 s_index = 1.0 646 else: 647 s_index = self.db.get_next_cc_series_num_for(val, 648 num=self.col_id) 649 self.idx_widget.setValue(s_index) 650 651 @property 652 def current_val(self): 653 val, s_index = self.gui_val 654 val = self.normalize_ui_val(val) 655 return val, s_index 656 657 def commit(self, book_id, notify=False): 658 val, s_index = self.current_val 659 if val != self.initial_val or s_index != self.initial_index: 660 if not val: 661 val = s_index = None 662 return self.db.set_custom(book_id, val, extra=s_index, num=self.col_id, 663 notify=notify, commit=False, allow_case_change=True) 664 else: 665 return set() 666 667 def apply_to_metadata(self, mi): 668 val, s_index = self.current_val 669 mi.set('#' + self.col_metadata['label'], val, extra=s_index) 670 671 def connect_data_changed(self, slot): 672 for s in self.name_widget.editTextChanged, self.idx_widget.valueChanged: 673 s.connect(slot) 674 self.signals_to_disconnect.append(s) 675 676 677class Enumeration(Base): 678 679 def setup_ui(self, parent): 680 self.parent = parent 681 self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)] 682 self.finish_ui_setup(parent, QComboBox) 683 vals = self.col_metadata['display']['enum_values'] 684 self.editor.addItem('') 685 for v in vals: 686 self.editor.addItem(v) 687 688 def initialize(self, book_id): 689 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 690 val = self.normalize_db_val(val) 691 idx = self.editor.findText(val) 692 if idx < 0: 693 error_dialog(self.parent, '', 694 _('The enumeration "{0}" contains an invalid value ' 695 'that will be set to the default').format( 696 self.col_metadata['name']), 697 show=True, show_copy_button=False) 698 699 idx = 0 700 self.editor.setCurrentIndex(idx) 701 self.initial_val = self.current_val 702 703 def setter(self, val): 704 self.editor.setCurrentIndex(self.editor.findText(val)) 705 706 def getter(self): 707 return str(self.editor.currentText()) 708 709 def normalize_db_val(self, val): 710 if val is None: 711 val = '' 712 return val 713 714 def normalize_ui_val(self, val): 715 if not val: 716 val = None 717 return val 718 719 def set_to_undefined(self): 720 self.editor.setCurrentIndex(0) 721 722 def connect_data_changed(self, slot): 723 self.editor.currentIndexChanged.connect(slot) 724 self.signals_to_disconnect.append(self.editor.currentIndexChanged) 725 726 727def comments_factory(db, key, parent): 728 fm = db.custom_column_num_map[key] 729 ctype = fm.get('display', {}).get('interpret_as', 'html') 730 if ctype == 'short-text': 731 return SimpleText(db, key, parent) 732 if ctype in ('long-text', 'markdown'): 733 return LongText(db, key, parent) 734 return Comments(db, key, parent) 735 736 737widgets = { 738 'bool' : Bool, 739 'rating' : Rating, 740 'int': Int, 741 'float': Float, 742 'datetime': DateTime, 743 'text' : Text, 744 'comments': comments_factory, 745 'series': Series, 746 'enumeration': Enumeration 747} 748 749 750def field_sort_key(y, fm=None): 751 m1 = fm[y] 752 name = icu_lower(m1['name']) 753 n1 = 'zzzzz' + name if m1['datatype'] == 'comments' and m1.get('display', {}).get('interpret_as') != 'short-text' else name 754 return sort_key(n1) 755 756 757def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None): 758 def widget_factory(typ, key): 759 if bulk: 760 w = bulk_widgets[typ](db, key, parent) 761 else: 762 w = widgets[typ](db, key, parent) 763 if book_id is not None: 764 w.initialize(book_id) 765 return w 766 fm = db.field_metadata 767 768 # Get list of all non-composite custom fields. We must make widgets for these 769 fields = fm.custom_field_keys(include_composites=False) 770 cols_to_display = fields 771 cols_to_display.sort(key=partial(field_sort_key, fm=fm)) 772 773 # This will contain the fields in the order to display them 774 cols = [] 775 776 # The fields named here must be first in the widget list 777 tweak_cols = tweaks['metadata_edit_custom_column_order'] 778 comments_in_tweak = 0 779 for key in (tweak_cols or ()): 780 # Add the key if it really exists in the database 781 if key in cols_to_display: 782 cols.append(key) 783 if fm[key]['datatype'] == 'comments' and fm[key].get('display', {}).get('interpret_as') != 'short-text': 784 comments_in_tweak += 1 785 786 # Add all the remaining fields 787 comments_not_in_tweak = 0 788 for key in cols_to_display: 789 if key not in cols: 790 cols.append(key) 791 if fm[key]['datatype'] == 'comments' and fm[key].get('display', {}).get('interpret_as') != 'short-text': 792 comments_not_in_tweak += 1 793 794 count = len(cols) 795 layout_rows_for_comments = 9 796 if two_column: 797 turnover_point = int(((count - comments_not_in_tweak + 1) + 798 int(comments_in_tweak*(layout_rows_for_comments-1)))/2) 799 else: 800 # Avoid problems with multi-line widgets 801 turnover_point = count + 1000 802 ans = [] 803 column = row = base_row = max_row = 0 804 label_width = 0 805 do_elision = tweaks['metadata_edit_elide_labels'] 806 elide_pos = tweaks['metadata_edit_elision_point'] 807 elide_pos = elide_pos if elide_pos in {'left', 'middle', 'right'} else 'right' 808 # make room on the right side for the scrollbar 809 sb_width = QApplication.instance().style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarExtent) 810 layout.setContentsMargins(0, 0, sb_width, 0) 811 for key in cols: 812 if not fm[key]['is_editable']: 813 continue # The job spy plugin can change is_editable 814 dt = fm[key]['datatype'] 815 if dt == 'composite' or (bulk and dt == 'comments'): 816 continue 817 is_comments = dt == 'comments' and fm[key].get('display', {}).get('interpret_as') != 'short-text' 818 w = widget_factory(dt, fm[key]['colnum']) 819 ans.append(w) 820 if two_column and is_comments: 821 # Here for compatibility with old layout. Comments always started 822 # in the left column 823 comments_in_tweak -= 1 824 # no special processing if the comment field was named in the tweak 825 if comments_in_tweak < 0 and comments_not_in_tweak > 0: 826 # Force a turnover, adding comments widgets below max_row. 827 # Save the row to return to if we turn over again 828 column = 0 829 row = max_row 830 base_row = row 831 turnover_point = row + int((comments_not_in_tweak * layout_rows_for_comments)/2) 832 comments_not_in_tweak = 0 833 834 l = QGridLayout() 835 if is_comments: 836 layout.addLayout(l, row, column, layout_rows_for_comments, 1) 837 layout.setColumnStretch(column, 100) 838 row += layout_rows_for_comments 839 else: 840 layout.addLayout(l, row, column, 1, 1) 841 layout.setColumnStretch(column, 100) 842 row += 1 843 for c in range(0, len(w.widgets), 2): 844 if not is_comments: 845 # Set the label column width to a fixed size. Elide labels that 846 # don't fit 847 wij = w.widgets[c] 848 if label_width == 0: 849 font_metrics = wij.fontMetrics() 850 colon_width = font_metrics.width(':') 851 if bulk: 852 label_width = (font_metrics.averageCharWidth() * 853 tweaks['metadata_edit_bulk_cc_label_length']) - colon_width 854 else: 855 label_width = (font_metrics.averageCharWidth() * 856 tweaks['metadata_edit_single_cc_label_length']) - colon_width 857 wij.setMaximumWidth(label_width) 858 if c == 0: 859 wij.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) 860 l.setColumnMinimumWidth(0, label_width) 861 wij.setAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignVCenter) 862 t = str(wij.text()) 863 if t: 864 if do_elision: 865 wij.setText(elided_text(t, font=font_metrics, 866 width=label_width, pos=elide_pos) + ':') 867 else: 868 wij.setText(t + ':') 869 wij.setWordWrap(True) 870 wij.setBuddy(w.widgets[c+1]) 871 l.addWidget(wij, c, 0) 872 l.addWidget(w.widgets[c+1], c, 1) 873 else: 874 l.addWidget(w.widgets[0], 0, 0, 1, 2) 875 max_row = max(max_row, row) 876 if row >= turnover_point: 877 column = 1 878 turnover_point = count + 1000 879 row = base_row 880 881 items = [] 882 if len(ans) > 0: 883 items.append(QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, 884 QSizePolicy.Policy.Expanding)) 885 layout.addItem(items[-1], layout.rowCount(), 0, 1, 1) 886 layout.setRowStretch(layout.rowCount()-1, 100) 887 return ans, items 888 889 890class BulkBase(Base): 891 892 @property 893 def gui_val(self): 894 if not hasattr(self, '_cached_gui_val_'): 895 self._cached_gui_val_ = self.getter() 896 return self._cached_gui_val_ 897 898 def get_initial_value(self, book_ids): 899 values = set() 900 for book_id in book_ids: 901 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 902 if isinstance(val, list): 903 val = frozenset(val) 904 values.add(val) 905 if len(values) > 1: 906 break 907 ans = None 908 if len(values) == 1: 909 ans = next(iter(values)) 910 if isinstance(ans, frozenset): 911 ans = list(ans) 912 return ans 913 914 def finish_ui_setup(self, parent, is_bool=False, add_edit_tags_button=(False,)): 915 self.was_none = False 916 l = self.widgets[1].layout() 917 if not is_bool or self.bools_are_tristate: 918 self.clear_button = QToolButton(parent) 919 self.clear_button.setIcon(QIcon(I('trash.png'))) 920 self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name'])) 921 self.clear_button.clicked.connect(self.set_to_undefined) 922 l.insertWidget(1, self.clear_button) 923 if is_bool: 924 self.set_no_button = QToolButton(parent) 925 self.set_no_button.setIcon(QIcon(I('list_remove.png'))) 926 self.set_no_button.clicked.connect(lambda:self.main_widget.setCurrentIndex(1)) 927 self.set_no_button.setToolTip(_('Set {0} to No').format(self.col_metadata['name'])) 928 l.insertWidget(1, self.set_no_button) 929 self.set_yes_button = QToolButton(parent) 930 self.set_yes_button.setIcon(QIcon(I('ok.png'))) 931 self.set_yes_button.clicked.connect(lambda:self.main_widget.setCurrentIndex(0)) 932 self.set_yes_button.setToolTip(_('Set {0} to Yes').format(self.col_metadata['name'])) 933 l.insertWidget(1, self.set_yes_button) 934 if add_edit_tags_button[0]: 935 self.edit_tags_button = QToolButton(parent) 936 self.edit_tags_button.setToolTip(_('Open Item editor')) 937 self.edit_tags_button.setIcon(QIcon(I('chapters.png'))) 938 self.edit_tags_button.clicked.connect(add_edit_tags_button[1]) 939 l.insertWidget(1, self.edit_tags_button) 940 l.insertStretch(2) 941 942 def initialize(self, book_ids): 943 self.initial_val = val = self.get_initial_value(book_ids) 944 val = self.normalize_db_val(val) 945 self.setter(val) 946 947 def commit(self, book_ids, notify=False): 948 if not self.a_c_checkbox.isChecked(): 949 return 950 val = self.gui_val 951 val = self.normalize_ui_val(val) 952 self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) 953 954 def make_widgets(self, parent, main_widget_class): 955 w = QWidget(parent) 956 self.widgets = [QLabel(label_string(self.col_metadata['name']), w), w] 957 l = QHBoxLayout() 958 l.setContentsMargins(0, 0, 0, 0) 959 w.setLayout(l) 960 self.main_widget = main_widget_class(w) 961 l.addWidget(self.main_widget) 962 l.setStretchFactor(self.main_widget, 10) 963 self.a_c_checkbox = QCheckBox(_('Apply changes'), w) 964 l.addWidget(self.a_c_checkbox) 965 self.ignore_change_signals = True 966 967 # connect to the various changed signals so we can auto-update the 968 # apply changes checkbox 969 if hasattr(self.main_widget, 'editTextChanged'): 970 # editable combobox widgets 971 self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed) 972 if hasattr(self.main_widget, 'textChanged'): 973 # lineEdit widgets 974 self.main_widget.textChanged.connect(self.a_c_checkbox_changed) 975 if hasattr(self.main_widget, 'currentIndexChanged'): 976 # combobox widgets 977 self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed) 978 if hasattr(self.main_widget, 'valueChanged'): 979 # spinbox widgets 980 self.main_widget.valueChanged.connect(self.a_c_checkbox_changed) 981 if hasattr(self.main_widget, 'dateTimeChanged'): 982 # dateEdit widgets 983 self.main_widget.dateTimeChanged.connect(self.a_c_checkbox_changed) 984 985 def a_c_checkbox_changed(self): 986 if not self.ignore_change_signals: 987 self.a_c_checkbox.setChecked(True) 988 989 990class BulkBool(BulkBase, Bool): 991 992 def get_initial_value(self, book_ids): 993 value = None 994 for book_id in book_ids: 995 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 996 if not self.db.new_api.pref('bools_are_tristate') and val is None: 997 val = False 998 if value is not None and value != val: 999 return None 1000 value = val 1001 return value 1002 1003 def setup_ui(self, parent): 1004 self.make_widgets(parent, QComboBox) 1005 items = [_('Yes'), _('No')] 1006 self.bools_are_tristate = self.db.new_api.pref('bools_are_tristate') 1007 if not self.bools_are_tristate: 1008 items.append('') 1009 else: 1010 items.append(_('Undefined')) 1011 icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] 1012 self.main_widget.blockSignals(True) 1013 for icon, text in zip(icons, items): 1014 self.main_widget.addItem(QIcon(icon), text) 1015 self.main_widget.blockSignals(False) 1016 self.finish_ui_setup(parent, is_bool=True) 1017 1018 def set_to_undefined(self): 1019 # Only called if bools are tristate 1020 self.main_widget.setCurrentIndex(2) 1021 1022 def getter(self): 1023 val = self.main_widget.currentIndex() 1024 if not self.bools_are_tristate: 1025 return {2: False, 1: False, 0: True}[val] 1026 else: 1027 return {2: None, 1: False, 0: True}[val] 1028 1029 def setter(self, val): 1030 val = {None: 2, False: 1, True: 0}[val] 1031 self.main_widget.setCurrentIndex(val) 1032 self.ignore_change_signals = False 1033 1034 def commit(self, book_ids, notify=False): 1035 if not self.a_c_checkbox.isChecked(): 1036 return 1037 val = self.gui_val 1038 val = self.normalize_ui_val(val) 1039 if not self.bools_are_tristate and val is None: 1040 val = False 1041 self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) 1042 1043 def a_c_checkbox_changed(self): 1044 if not self.ignore_change_signals: 1045 if not self.bools_are_tristate and self.main_widget.currentIndex() == 2: 1046 self.a_c_checkbox.setChecked(False) 1047 else: 1048 self.a_c_checkbox.setChecked(True) 1049 1050 1051class BulkInt(BulkBase): 1052 1053 def setup_ui(self, parent): 1054 self.make_widgets(parent, QSpinBox) 1055 self.main_widget.setRange(-1000000, 100000000) 1056 self.finish_ui_setup(parent) 1057 1058 def finish_ui_setup(self, parent): 1059 BulkBase.finish_ui_setup(self, parent) 1060 self.main_widget.setSpecialValueText(_('Undefined')) 1061 self.main_widget.setSingleStep(1) 1062 self.main_widget.valueChanged.connect(self.valueChanged) 1063 1064 def setter(self, val): 1065 if val is None: 1066 val = self.main_widget.minimum() 1067 self.main_widget.setValue(val) 1068 self.ignore_change_signals = False 1069 self.was_none = val == self.main_widget.minimum() 1070 1071 def getter(self): 1072 val = self.main_widget.value() 1073 if val == self.main_widget.minimum(): 1074 val = None 1075 return val 1076 1077 def valueChanged(self, to_what): 1078 if self.was_none and to_what == -999999: 1079 self.setter(0) 1080 self.was_none = to_what == self.main_widget.minimum() 1081 1082 def set_to_undefined(self): 1083 self.main_widget.setValue(-1000000) 1084 1085 1086class BulkFloat(BulkInt): 1087 1088 def setup_ui(self, parent): 1089 self.make_widgets(parent, QDoubleSpinBox) 1090 self.main_widget.setRange(-1000000., float(100000000)) 1091 self.main_widget.setDecimals(2) 1092 self.finish_ui_setup(parent) 1093 1094 def set_to_undefined(self): 1095 self.main_widget.setValue(-1000000.) 1096 1097 1098class BulkRating(BulkBase): 1099 1100 def setup_ui(self, parent): 1101 allow_half_stars = self.col_metadata['display'].get('allow_half_stars', False) 1102 self.make_widgets(parent, partial(RatingEditor, is_half_star=allow_half_stars)) 1103 self.finish_ui_setup(parent) 1104 1105 def set_to_undefined(self): 1106 self.main_widget.setCurrentIndex(0) 1107 1108 def setter(self, val): 1109 val = max(0, min(int(val or 0), 10)) 1110 self.main_widget.rating_value = val 1111 self.ignore_change_signals = False 1112 1113 def getter(self): 1114 return self.main_widget.rating_value or None 1115 1116 1117class BulkDateTime(BulkBase): 1118 1119 def setup_ui(self, parent): 1120 cm = self.col_metadata 1121 self.make_widgets(parent, DateTimeEdit) 1122 l = self.widgets[1].layout() 1123 self.today_button = QToolButton(parent) 1124 self.today_button.setText(_('Today')) 1125 l.insertWidget(1, self.today_button) 1126 self.clear_button = QToolButton(parent) 1127 self.clear_button.setIcon(QIcon(I('trash.png'))) 1128 self.clear_button.setToolTip(_('Clear {0}').format(self.col_metadata['name'])) 1129 l.insertWidget(2, self.clear_button) 1130 l.insertStretch(3) 1131 1132 w = self.main_widget 1133 format_ = cm['display'].get('date_format','') 1134 if not format_: 1135 format_ = 'dd MMM yyyy' 1136 elif format_ == 'iso': 1137 format_ = internal_iso_format_string() 1138 w.setDisplayFormat(format_) 1139 w.setCalendarPopup(True) 1140 w.setMinimumDateTime(UNDEFINED_QDATETIME) 1141 w.setSpecialValueText(_('Undefined')) 1142 self.today_button.clicked.connect(w.set_to_today) 1143 self.clear_button.clicked.connect(w.set_to_clear) 1144 1145 def setter(self, val): 1146 if val is None: 1147 val = self.main_widget.minimumDateTime() 1148 else: 1149 val = QDateTime(val) 1150 self.main_widget.setDateTime(val) 1151 self.ignore_change_signals = False 1152 1153 def getter(self): 1154 val = self.main_widget.dateTime() 1155 if val <= UNDEFINED_QDATETIME: 1156 val = None 1157 else: 1158 val = qt_to_dt(val) 1159 return val 1160 1161 def normalize_db_val(self, val): 1162 return as_local_time(val) if val is not None else None 1163 1164 def normalize_ui_val(self, val): 1165 return as_utc(val) if val is not None else None 1166 1167 1168class BulkSeries(BulkBase): 1169 1170 def setup_ui(self, parent): 1171 self.make_widgets(parent, EditWithComplete) 1172 values = self.all_values = list(self.db.all_custom(num=self.col_id)) 1173 values.sort(key=sort_key) 1174 self.main_widget.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) 1175 self.main_widget.setMinimumContentsLength(25) 1176 self.widgets.append(QLabel('', parent)) 1177 w = QWidget(parent) 1178 layout = QHBoxLayout(w) 1179 layout.setContentsMargins(0, 0, 0, 0) 1180 self.remove_series = QCheckBox(parent) 1181 self.remove_series.setText(_('Clear series')) 1182 layout.addWidget(self.remove_series) 1183 self.idx_widget = QCheckBox(parent) 1184 self.idx_widget.setText(_('Automatically number books')) 1185 self.idx_widget.setToolTip('<p>' + _( 1186 'If not checked, the series number for the books will be set to 1. ' 1187 'If checked, selected books will be automatically numbered, ' 1188 'in the order you selected them. So if you selected ' 1189 'Book A and then Book B, Book A will have series number 1 ' 1190 'and Book B series number 2.') + '</p>') 1191 layout.addWidget(self.idx_widget) 1192 self.force_number = QCheckBox(parent) 1193 self.force_number.setText(_('Force numbers to start with ')) 1194 self.force_number.setToolTip('<p>' + _( 1195 'Series will normally be renumbered from the highest ' 1196 'number in the database for that series. Checking this ' 1197 'box will tell calibre to start numbering from the value ' 1198 'in the box') + '</p>') 1199 layout.addWidget(self.force_number) 1200 self.series_start_number = QDoubleSpinBox(parent) 1201 self.series_start_number.setMinimum(0.0) 1202 self.series_start_number.setMaximum(9999999.0) 1203 self.series_start_number.setProperty("value", 1.0) 1204 layout.addWidget(self.series_start_number) 1205 self.series_increment = QDoubleSpinBox(parent) 1206 self.series_increment.setMinimum(0.00) 1207 self.series_increment.setMaximum(99999.0) 1208 self.series_increment.setProperty("value", 1.0) 1209 self.series_increment.setToolTip('<p>' + _( 1210 'The amount by which to increment the series number ' 1211 'for successive books. Only applicable when using ' 1212 'force series numbers.') + '</p>') 1213 self.series_increment.setPrefix('+') 1214 layout.addWidget(self.series_increment) 1215 layout.addItem(QSpacerItem(20, 10, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)) 1216 self.widgets.append(w) 1217 self.idx_widget.stateChanged.connect(self.a_c_checkbox_changed) 1218 self.force_number.stateChanged.connect(self.a_c_checkbox_changed) 1219 self.series_start_number.valueChanged.connect(self.a_c_checkbox_changed) 1220 self.series_increment.valueChanged.connect(self.a_c_checkbox_changed) 1221 self.remove_series.stateChanged.connect(self.a_c_checkbox_changed) 1222 self.main_widget 1223 self.ignore_change_signals = False 1224 1225 def a_c_checkbox_changed(self): 1226 def disable_numbering_checkboxes(idx_widget_enable): 1227 if idx_widget_enable: 1228 self.idx_widget.setEnabled(True) 1229 else: 1230 self.idx_widget.setChecked(False) 1231 self.idx_widget.setEnabled(False) 1232 self.force_number.setChecked(False) 1233 self.force_number.setEnabled(False) 1234 self.series_start_number.setEnabled(False) 1235 self.series_increment.setEnabled(False) 1236 1237 if self.ignore_change_signals: 1238 return 1239 self.ignore_change_signals = True 1240 apply_changes = False 1241 if self.remove_series.isChecked(): 1242 self.main_widget.setText('') 1243 self.main_widget.setEnabled(False) 1244 disable_numbering_checkboxes(idx_widget_enable=False) 1245 apply_changes = True 1246 elif self.main_widget.text(): 1247 self.remove_series.setEnabled(False) 1248 self.idx_widget.setEnabled(True) 1249 apply_changes = True 1250 else: # no text, no clear. Basically reinitialize 1251 self.main_widget.setEnabled(True) 1252 self.remove_series.setEnabled(True) 1253 disable_numbering_checkboxes(idx_widget_enable=False) 1254 apply_changes = False 1255 1256 self.force_number.setEnabled(self.idx_widget.isChecked()) 1257 self.series_start_number.setEnabled(self.force_number.isChecked()) 1258 self.series_increment.setEnabled(self.force_number.isChecked()) 1259 1260 self.ignore_change_signals = False 1261 self.a_c_checkbox.setChecked(apply_changes) 1262 1263 def initialize(self, book_id): 1264 self.idx_widget.setChecked(False) 1265 self.main_widget.set_separator(None) 1266 self.main_widget.update_items_cache(self.all_values) 1267 self.main_widget.setEditText('') 1268 self.a_c_checkbox.setChecked(False) 1269 1270 def getter(self): 1271 n = str(self.main_widget.currentText()).strip() 1272 autonumber = self.idx_widget.checkState() 1273 force = self.force_number.checkState() 1274 start = self.series_start_number.value() 1275 remove = self.remove_series.checkState() 1276 increment = self.series_increment.value() 1277 return n, autonumber, force, start, remove, increment 1278 1279 def commit(self, book_ids, notify=False): 1280 if not self.a_c_checkbox.isChecked(): 1281 return 1282 val, update_indices, force_start, at_value, clear, increment = self.gui_val 1283 val = None if clear else self.normalize_ui_val(val) 1284 if clear or val != '': 1285 extras = [] 1286 for book_id in book_ids: 1287 if clear: 1288 extras.append(None) 1289 continue 1290 if update_indices: 1291 if force_start: 1292 s_index = at_value 1293 at_value += increment 1294 elif tweaks['series_index_auto_increment'] != 'const': 1295 s_index = self.db.get_next_cc_series_num_for(val, num=self.col_id) 1296 else: 1297 s_index = 1.0 1298 else: 1299 s_index = self.db.get_custom_extra(book_id, num=self.col_id, 1300 index_is_id=True) 1301 extras.append(s_index) 1302 self.db.set_custom_bulk(book_ids, val, extras=extras, 1303 num=self.col_id, notify=notify) 1304 1305 1306class BulkEnumeration(BulkBase, Enumeration): 1307 1308 def get_initial_value(self, book_ids): 1309 value = None 1310 first = True 1311 dialog_shown = False 1312 for book_id in book_ids: 1313 val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 1314 if val and val not in self.col_metadata['display']['enum_values']: 1315 if not dialog_shown: 1316 error_dialog(self.parent, '', 1317 _('The enumeration "{0}" contains invalid values ' 1318 'that will not appear in the list').format( 1319 self.col_metadata['name']), 1320 show=True, show_copy_button=False) 1321 dialog_shown = True 1322 if first: 1323 value = val 1324 first = False 1325 elif value != val: 1326 value = None 1327 if not value: 1328 self.ignore_change_signals = False 1329 return value 1330 1331 def setup_ui(self, parent): 1332 self.parent = parent 1333 self.make_widgets(parent, QComboBox) 1334 self.finish_ui_setup(parent) 1335 vals = self.col_metadata['display']['enum_values'] 1336 self.main_widget.blockSignals(True) 1337 self.main_widget.addItem('') 1338 self.main_widget.addItems(vals) 1339 self.main_widget.blockSignals(False) 1340 1341 def set_to_undefined(self): 1342 self.main_widget.setCurrentIndex(0) 1343 1344 def getter(self): 1345 return str(self.main_widget.currentText()) 1346 1347 def setter(self, val): 1348 if val is None: 1349 self.main_widget.setCurrentIndex(0) 1350 else: 1351 self.main_widget.setCurrentIndex(self.main_widget.findText(val)) 1352 self.ignore_change_signals = False 1353 1354 1355class RemoveTags(QWidget): 1356 1357 def __init__(self, parent, values): 1358 QWidget.__init__(self, parent) 1359 layout = QHBoxLayout() 1360 layout.setSpacing(5) 1361 layout.setContentsMargins(0, 0, 0, 0) 1362 1363 self.tags_box = EditWithComplete(parent) 1364 self.tags_box.update_items_cache(values) 1365 layout.addWidget(self.tags_box, stretch=3) 1366 self.remove_tags_button = QToolButton(parent) 1367 self.remove_tags_button.setToolTip(_('Open Item editor')) 1368 self.remove_tags_button.setIcon(QIcon(I('chapters.png'))) 1369 layout.addWidget(self.remove_tags_button) 1370 self.checkbox = QCheckBox(_('Remove all tags'), parent) 1371 layout.addWidget(self.checkbox) 1372 layout.addStretch(1) 1373 self.setLayout(layout) 1374 self.checkbox.stateChanged[int].connect(self.box_touched) 1375 1376 def box_touched(self, state): 1377 if state: 1378 self.tags_box.setText('') 1379 self.tags_box.setEnabled(False) 1380 else: 1381 self.tags_box.setEnabled(True) 1382 1383 1384class BulkText(BulkBase): 1385 1386 def setup_ui(self, parent): 1387 values = self.all_values = list(self.db.all_custom(num=self.col_id)) 1388 values.sort(key=sort_key) 1389 is_tags = False 1390 if self.col_metadata['is_multiple']: 1391 is_tags = not self.col_metadata['display'].get('is_names', False) 1392 self.make_widgets(parent, EditWithComplete) 1393 self.main_widget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) 1394 self.adding_widget = self.main_widget 1395 1396 if is_tags: 1397 w = RemoveTags(parent, values) 1398 w.remove_tags_button.clicked.connect(self.edit_remove) 1399 l = QLabel(label_string(self.col_metadata['name'])+': ' + 1400 _('tags to remove'), parent) 1401 tt = get_tooltip(self.col_metadata) + ': ' + _('tags to remove') 1402 l.setToolTip(tt) 1403 self.widgets.append(l) 1404 w.setToolTip(tt) 1405 self.widgets.append(w) 1406 self.removing_widget = w 1407 self.main_widget.set_separator(',') 1408 w.tags_box.textChanged.connect(self.a_c_checkbox_changed) 1409 w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) 1410 else: 1411 self.main_widget.set_separator('&') 1412 self.main_widget.set_space_before_sep(True) 1413 self.main_widget.set_add_separator( 1414 tweaks['authors_completer_append_separator']) 1415 else: 1416 self.make_widgets(parent, EditWithComplete) 1417 self.main_widget.set_separator(None) 1418 self.main_widget.setSizeAdjustPolicy( 1419 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) 1420 self.main_widget.setMinimumContentsLength(25) 1421 self.ignore_change_signals = False 1422 self.parent = parent 1423 self.finish_ui_setup(parent, add_edit_tags_button=(is_tags,self.edit_add)) 1424 1425 def set_to_undefined(self): 1426 self.main_widget.clearEditText() 1427 1428 def initialize(self, book_ids): 1429 self.main_widget.update_items_cache(self.all_values) 1430 if not self.col_metadata['is_multiple']: 1431 val = self.get_initial_value(book_ids) 1432 self.initial_val = val = self.normalize_db_val(val) 1433 self.ignore_change_signals = True 1434 self.main_widget.blockSignals(True) 1435 self.main_widget.setText(val) 1436 self.main_widget.blockSignals(False) 1437 self.ignore_change_signals = False 1438 1439 def commit(self, book_ids, notify=False): 1440 if not self.a_c_checkbox.isChecked(): 1441 return 1442 if self.col_metadata['is_multiple']: 1443 ism = self.col_metadata['multiple_seps'] 1444 if self.col_metadata['display'].get('is_names', False): 1445 val = self.gui_val 1446 add = [v.strip() for v in val.split(ism['ui_to_list']) if v.strip()] 1447 self.db.set_custom_bulk(book_ids, add, num=self.col_id) 1448 else: 1449 remove_all, adding, rtext = self.gui_val 1450 remove = set() 1451 if remove_all: 1452 remove = set(self.db.all_custom(num=self.col_id)) 1453 else: 1454 txt = rtext 1455 if txt: 1456 remove = {v.strip() for v in txt.split(ism['ui_to_list'])} 1457 txt = adding 1458 if txt: 1459 add = {v.strip() for v in txt.split(ism['ui_to_list'])} 1460 else: 1461 add = set() 1462 self.db.set_custom_bulk_multiple(book_ids, add=add, 1463 remove=remove, num=self.col_id) 1464 else: 1465 val = self.gui_val 1466 val = self.normalize_ui_val(val) 1467 self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) 1468 1469 def getter(self): 1470 if self.col_metadata['is_multiple']: 1471 if not self.col_metadata['display'].get('is_names', False): 1472 return self.removing_widget.checkbox.isChecked(), \ 1473 str(self.adding_widget.text()), \ 1474 str(self.removing_widget.tags_box.text()) 1475 return str(self.adding_widget.text()) 1476 val = str(self.main_widget.currentText()).strip() 1477 if not val: 1478 val = None 1479 return val 1480 1481 def edit_remove(self): 1482 self.edit(widget=self.removing_widget.tags_box) 1483 1484 def edit_add(self): 1485 self.edit(widget=self.main_widget) 1486 1487 def edit(self, widget): 1488 if widget.text(): 1489 d = _save_dialog(self.parent, _('Values changed'), 1490 _('You have entered values. In order to use this ' 1491 'editor you must first discard them. ' 1492 'Discard the values?')) 1493 if d == QMessageBox.StandardButton.Cancel or d == QMessageBox.StandardButton.No: 1494 return 1495 widget.setText('') 1496 d = TagEditor(self.parent, self.db, key=('#'+self.col_metadata['label'])) 1497 if d.exec() == QDialog.DialogCode.Accepted: 1498 val = d.tags 1499 if not val: 1500 val = [] 1501 widget.setText(self.col_metadata['multiple_seps']['list_to_ui'].join(val)) 1502 1503 1504bulk_widgets = { 1505 'bool' : BulkBool, 1506 'rating' : BulkRating, 1507 'int': BulkInt, 1508 'float': BulkFloat, 1509 'datetime': BulkDateTime, 1510 'text' : BulkText, 1511 'series': BulkSeries, 1512 'enumeration': BulkEnumeration, 1513} 1514