class MultipleWidget(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) layout = QHBoxLayout() layout.setSpacing(5) layout.setContentsMargins(0, 0, 0, 0) self.tags_box = EditWithComplete(parent) layout.addWidget(self.tags_box, stretch=1000) self.editor_button = QToolButton(self) self.editor_button.setToolTip(_('Open Item Editor')) self.editor_button.setIcon(QIcon(I('chapters.png'))) layout.addWidget(self.editor_button) self.setLayout(layout) def get_editor_button(self): return self.editor_button def update_items_cache(self, values): self.tags_box.update_items_cache(values) def clear(self): self.tags_box.clear() def setEditText(self): self.tags_box.setEditText() def addItem(self, itm): self.tags_box.addItem(itm) def set_separator(self, sep): self.tags_box.set_separator(sep) def set_add_separator(self, sep): self.tags_box.set_add_separator(sep) def set_space_before_sep(self, v): self.tags_box.set_space_before_sep(v) def setSizePolicy(self, v1, v2): self.tags_box.setSizePolicy(v1, v2) def setText(self, v): self.tags_box.setText(v) def text(self): return self.tags_box.text()
class SeriesDialog(SizePersistedDialog): def __init__(self, parent, books, all_series, series_columns): SizePersistedDialog.__init__(self, parent, 'Manage Series plugin:series dialog') self.db = self.parent().library_view.model().db self.books = books self.all_series = all_series self.series_columns = series_columns self.block_events = True self.initialize_controls() # Books will have been sorted by the Calibre series column # Choose the appropriate series column to be editing initial_series_column = 'Series' self.series_column_combo.select_text(initial_series_column) if len(series_columns) == 0: # Will not have fired the series_column_changed event self.series_column_changed() # Renumber the books using the assigned series name/index in combos/spinbox self.renumber_series(display_in_table=False) # Display the books in the table self.block_events = False self.series_table.populate_table(books) if len(unicode(self.series_combo.text()).strip()) > 0: self.series_table.setFocus() else: self.series_combo.setFocus() self.update_series_headers(initial_series_column) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() def initialize_controls(self): self.setWindowTitle('Manage Series') layout = QVBoxLayout(self) self.setLayout(layout) title_layout = ImageTitleLayout(self, 'images/manage_series.png', 'Create or Modify Series') layout.addLayout(title_layout) # Series name and start index layout series_name_layout = QHBoxLayout() layout.addLayout(series_name_layout) series_column_label = QLabel('Series &Column:', self) series_name_layout.addWidget(series_column_label) self.series_column_combo = SeriesColumnComboBox( self, self.series_columns) self.series_column_combo.currentIndexChanged[int].connect( self.series_column_changed) series_name_layout.addWidget(self.series_column_combo) series_column_label.setBuddy(self.series_column_combo) series_name_layout.addSpacing(20) series_label = QLabel('Series &Name:', self) series_name_layout.addWidget(series_label) self.series_combo = EditWithComplete(self) self.series_combo.setEditable(True) self.series_combo.setInsertPolicy(QtGui.QComboBox.InsertAlphabetically) self.series_combo.setSizeAdjustPolicy( QtGui.QComboBox.AdjustToMinimumContentsLengthWithIcon) self.series_combo.setMinimumContentsLength(25) self.series_combo.currentIndexChanged[int].connect(self.series_changed) self.series_combo.editTextChanged.connect(self.series_changed) self.series_combo.set_separator(None) series_label.setBuddy(self.series_combo) series_name_layout.addWidget(self.series_combo) series_name_layout.addSpacing(20) series_start_label = QLabel('&Start At:', self) series_name_layout.addWidget(series_start_label) self.series_start_number = QtGui.QSpinBox(self) self.series_start_number.setRange(0, 99000000) self.series_start_number.valueChanged[int].connect( self.series_start_changed) series_name_layout.addWidget(self.series_start_number) series_start_label.setBuddy(self.series_start_number) series_name_layout.insertStretch(-1) # Main series table layout table_layout = QHBoxLayout() layout.addLayout(table_layout) self.series_table = SeriesTableWidget(self) self.series_table.itemSelectionChanged.connect( self.item_selection_changed) self.series_table.cellChanged[int, int].connect(self.cell_changed) table_layout.addWidget(self.series_table) table_button_layout = QVBoxLayout() table_layout.addLayout(table_button_layout) move_up_button = QtGui.QToolButton(self) move_up_button.setToolTip('Move book up in series (Alt+Up)') move_up_button.setIcon(get_icon('arrow-up.png')) move_up_button.setShortcut(_('Alt+Up')) move_up_button.clicked.connect(self.move_rows_up) table_button_layout.addWidget(move_up_button) move_down_button = QtGui.QToolButton(self) move_down_button.setToolTip('Move book down in series (Alt+Down)') move_down_button.setIcon(get_icon('arrow-down.png')) move_down_button.setShortcut(_('Alt+Down')) move_down_button.clicked.connect(self.move_rows_down) table_button_layout.addWidget(move_down_button) spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) table_button_layout.addItem(spacerItem1) assign_index_button = QtGui.QToolButton(self) assign_index_button.setToolTip('Lock to index value...') assign_index_button.setIcon(get_icon('images/lock.png')) assign_index_button.clicked.connect(self.assign_index) table_button_layout.addWidget(assign_index_button) clear_index_button = QtGui.QToolButton(self) clear_index_button.setToolTip('Unlock series index') clear_index_button.setIcon(get_icon('images/lock_delete.png')) clear_index_button.clicked.connect(self.clear_index) table_button_layout.addWidget(clear_index_button) spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) table_button_layout.addItem(spacerItem2) add_empty_button = QtGui.QToolButton(self) add_empty_button.setToolTip('Add empty book to the series list') add_empty_button.setIcon(get_icon('add_book.png')) add_empty_button.setShortcut(_('Ctrl+Shift+E')) add_empty_button.clicked.connect(self.add_empty_book) table_button_layout.addWidget(add_empty_button) delete_button = QtGui.QToolButton(self) delete_button.setToolTip('Remove book from the series list') delete_button.setIcon(get_icon('trash.png')) delete_button.clicked.connect(self.remove_book) table_button_layout.addWidget(delete_button) spacerItem3 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) table_button_layout.addItem(spacerItem3) move_left_button = QtGui.QToolButton(self) move_left_button.setToolTip( 'Move series index to left of decimal point (Alt+Left)') move_left_button.setIcon(get_icon('back.png')) move_left_button.setShortcut(_('Alt+Left')) move_left_button.clicked.connect(partial(self.series_indent_change, -1)) table_button_layout.addWidget(move_left_button) move_right_button = QtGui.QToolButton(self) move_right_button.setToolTip( 'Move series index to right of decimal point (Alt+Right)') move_right_button.setIcon(get_icon('forward.png')) move_right_button.setShortcut(_('Alt+Right')) move_right_button.clicked.connect(partial(self.series_indent_change, 1)) table_button_layout.addWidget(move_right_button) # Dialog buttons button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) keep_button = button_box.addButton(' &Restore Original Series ', QDialogButtonBox.ResetRole) keep_button.clicked.connect(self.restore_original_series) def series_column_changed(self): series_column = self.series_column_combo.selected_value() SeriesBook.series_column = series_column # Choose a series name and series index from the first book in the list initial_series_name = '' initial_series_index = 1 if len(self.books) > 0: first_book = self.books[0] initial_series_name = first_book.series_name() if initial_series_name: initial_series_index = int(first_book.series_index()) # Populate the series name combo as appropriate for that column self.initialize_series_name_combo(series_column, initial_series_name) # Populate the series index spinbox with the initial value self.series_start_number.setProperty('value', initial_series_index) self.update_series_headers(series_column) if self.block_events: return self.renumber_series() def update_series_headers(self, series_column): if series_column == 'Series': self.series_table.set_series_column_headers(series_column) else: header_text = self.series_columns[series_column]['name'] self.series_table.set_series_column_headers(header_text) def initialize_series_name_combo(self, series_column, series_name): self.series_combo.clear() if series_name is None: series_name = '' values = self.all_series if series_column == 'Series': self.series_combo.update_items_cache([x[1] for x in values]) for i in values: _id, name = i self.series_combo.addItem(name) else: label = self.db.field_metadata.key_to_label(series_column) values = list(self.db.all_custom(label=label)) values.sort(key=sort_key) self.series_combo.update_items_cache(values) for name in values: self.series_combo.addItem(name) self.series_combo.setEditText(series_name) def series_changed(self): if self.block_events: return self.renumber_series() def series_start_changed(self): if self.block_events: return self.renumber_series() def restore_original_series(self): # Go through the books and overwrite the indexes with the originals, fixing in place for book in self.books: if book.orig_series_index(): book.set_assigned_index(book.orig_series_index()) #book.set_series_name(book.orig_series_name()) book.set_series_index(book.orig_series_index()) # Now renumber the whole series so that anything in between gets changed self.renumber_series() def renumber_series(self, display_in_table=True): if len(self.books) == 0: return series_name = unicode(self.series_combo.currentText()).strip() series_index = float(unicode(self.series_start_number.value())) last_series_indent = 0 for row, book in enumerate(self.books): book.set_series_name(series_name) series_indent = book.series_indent() if book.assigned_index() is not None: series_index = book.assigned_index() else: if series_indent >= last_series_indent: if series_indent == 0: if row > 0: series_index += 1. elif series_indent == 1: series_index += 0.1 else: series_index += 0.01 else: # When series indent decreases, need to round to next if series_indent == 1: series_index = round(series_index + 0.05, 1) else: # series_indent == 0: series_index = round(series_index + 0.5, 0) book.set_series_index(series_index) last_series_indent = series_indent # Now determine whether books have a valid index or not self.books[0].set_is_valid(True) for row in range(len(self.books) - 1, 0, -1): book = self.books[row] previous_book = self.books[row - 1] if book.series_index() <= previous_book.series_index(): book.set_is_valid(False) else: book.set_is_valid(True) if display_in_table: for row, book in enumerate(self.books): self.series_table.populate_table_row(row, book) def assign_original_index(self): if len(self.books) == 0: return for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] book.set_assigned_index(book.orig_series_index()) self.renumber_series() self.item_selection_changed() def assign_index(self): if len(self.books) == 0: return auto_assign_value = None for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] if auto_assign_value is not None: book.set_assigned_index(auto_assign_value) continue d = LockSeriesDialog(self, book.title(), book.series_index()) d.exec_() if d.result() != d.Accepted: break if d.assign_same_value(): auto_assign_value = d.get_value() book.set_assigned_index(auto_assign_value) else: book.set_assigned_index(d.get_value()) self.renumber_series() self.item_selection_changed() def clear_index(self, all_rows=False): if len(self.books) == 0: return if all_rows: for book in self.books: book.set_assigned_index(None) else: for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] book.set_assigned_index(None) self.renumber_series() def add_empty_book(self): def create_empty_book(authors): mi = Metadata(_('Unknown'), dlg.selected_authors) for key in self.series_columns.keys(): meta = self.db.metadata_for_field(key) mi.set_user_metadata(key, meta) mi.set(key, val=None, extra=None) return SeriesBook(mi, self.series_columns) idx = self.series_table.currentRow() if idx == -1: author = None else: author = self.books[idx].authors()[0] dlg = AddEmptyBookDialog(self, self.db, author) if dlg.exec_() != dlg.Accepted: return num = dlg.qty_to_add for _x in range(num): idx += 1 book = create_empty_book(dlg.selected_authors) self.books.insert(idx, book) self.series_table.setRowCount(len(self.books)) self.renumber_series() def remove_book(self): if not question_dialog( self, _('Are you sure?'), '<p>' + 'Remove the selected book(s) from the series list?', show_copy_button=False): return rows = self.series_table.selectionModel().selectedRows() if len(rows) == 0: return selrows = [] for row in rows: selrows.append(row.row()) selrows.sort() first_sel_row = self.series_table.currentRow() for row in reversed(selrows): self.books.pop(row) self.series_table.removeRow(row) if first_sel_row < self.series_table.rowCount(): self.series_table.select_and_scroll_to_row(first_sel_row) elif self.series_table.rowCount() > 0: self.series_table.select_and_scroll_to_row(first_sel_row - 1) self.renumber_series() def move_rows_up(self): self.series_table.setFocus() rows = self.series_table.selectionModel().selectedRows() if len(rows) == 0: return first_sel_row = rows[0].row() if first_sel_row <= 0: return # Workaround for strange selection bug in Qt which "alters" the selection # in certain circumstances which meant move down only worked properly "once" selrows = [] for row in rows: selrows.append(row.row()) selrows.sort() for selrow in selrows: self.series_table.swap_row_widgets(selrow - 1, selrow + 1) self.books[selrow - 1], self.books[selrow] = self.books[selrow], self.books[ selrow - 1] scroll_to_row = first_sel_row - 1 if scroll_to_row > 0: scroll_to_row = scroll_to_row - 1 self.series_table.scrollToItem(self.series_table.item( scroll_to_row, 0)) self.renumber_series() def move_rows_down(self): self.series_table.setFocus() rows = self.series_table.selectionModel().selectedRows() if len(rows) == 0: return last_sel_row = rows[-1].row() if last_sel_row == self.series_table.rowCount() - 1: return # Workaround for strange selection bug in Qt which "alters" the selection # in certain circumstances which meant move down only worked properly "once" selrows = [] for row in rows: selrows.append(row.row()) selrows.sort() for selrow in reversed(selrows): self.series_table.swap_row_widgets(selrow + 2, selrow) self.books[selrow + 1], self.books[selrow] = self.books[selrow], self.books[ selrow + 1] scroll_to_row = last_sel_row + 1 if scroll_to_row < self.series_table.rowCount() - 1: scroll_to_row = scroll_to_row + 1 self.series_table.scrollToItem(self.series_table.item( scroll_to_row, 0)) self.renumber_series() def series_indent_change(self, delta): for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] series_indent = book.series_indent() if delta > 0: if series_indent < 2: book.set_series_indent(series_indent + 1) else: if series_indent > 0: book.set_series_indent(series_indent - 1) book.set_assigned_index(None) self.renumber_series() def sort_by(self, name): if name == 'PubDate': self.books = sorted(self.books, key=lambda k: k.sort_key(sort_by_pubdate=True)) elif name == 'Original Series Name': self.books = sorted(self.books, key=lambda k: k.sort_key(sort_by_name=True)) else: self.books = sorted(self.books, key=lambda k: k.sort_key()) self.renumber_series() def search_web(self, name): URLS = { 'FantasticFiction': 'http://www.fantasticfiction.co.uk/search/?searchfor=author&keywords={author}', 'Goodreads': 'http://www.goodreads.com/search/search?q={author}&search_type=books', 'Google': 'http://www.google.com/#sclient=psy&q=%22{author}%22+%22{title}%22', 'Wikipedia': 'http://en.wikipedia.org/w/index.php?title=Special%3ASearch&search={author}' } for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] safe_title = self.convert_to_search_text(book.title()) safe_author = self.convert_author_to_search_text(book.authors()[0]) url = URLS[name].replace('{title}', safe_title).replace( '{author}', safe_author) open_url(QUrl.fromEncoded(url)) def convert_to_search_text(self, text, encoding='utf-8'): # First we strip characters we will definitely not want to pass through. # Periods from author initials etc do not need to be supplied text = text.replace('.', '') # Now encode the text using Python function with chosen encoding text = quote_plus(text.encode(encoding, 'ignore')) # If we ended up with double spaces as plus signs (++) replace them text = text.replace('++', '+') return text def convert_author_to_search_text(self, author, encoding='utf-8'): # We want to convert the author name to FN LN format if it is stored LN, FN # We do this because some websites (Kobo) have crappy search engines that # will not match Adams+Douglas but will match Douglas+Adams # Not really sure of the best way of determining if the user is using LN, FN # Approach will be to check the tweak and see if a comma is in the name # Comma separated author will be pipe delimited in Calibre database fn_ln_author = author if author.find(',') > -1: # This might be because of a FN LN,Jr - check the tweak sort_copy_method = tweaks['author_sort_copy_method'] if sort_copy_method == 'invert': # Calibre default. Hence "probably" using FN LN format. fn_ln_author = author else: # We will assume that we need to switch the names from LN,FN to FN LN parts = author.split(',') surname = parts.pop(0) parts.append(surname) fn_ln_author = ' '.join(parts).strip() return self.convert_to_search_text(fn_ln_author, encoding) def cell_changed(self, row, column): book = self.books[row] if column == 0: book.set_title( unicode(self.series_table.item(row, column).text()).strip()) elif column == 2: qtdate = convert_qvariant( self.series_table.item(row, column).data(Qt.DisplayRole)) book.set_pubdate(qt_to_dt(qtdate, as_utc=False)) def item_selection_changed(self): row = self.series_table.currentRow() if row == -1: return has_assigned_index = False for row in self.series_table.selectionModel().selectedRows(): book = self.books[row.row()] if book.assigned_index(): has_assigned_index = True self.series_table.clear_index_action.setEnabled(has_assigned_index) if not has_assigned_index: for book in self.books: if book.assigned_index(): has_assigned_index = True self.series_table.clear_all_index_action.setEnabled(has_assigned_index)