class AnnotationElementsTable(QTableWidget): ''' QTableWidget managing CSS rules ''' DEBUG = True #MAXIMUM_TABLE_HEIGHT = 113 ELEMENT_FIELD_WIDTH = 250 if isosx: FONT = QFont('Monaco', 11) elif iswindows: FONT = QFont('Lucida Console', 9) elif islinux: FONT = QFont('Monospace', 9) FONT.setStyleHint(QFont.TypeWriter) COLUMNS = { 'ELEMENT_NAME': {'ordinal': 0, 'name': 'Element Name'}, 'ELEMENT': {'ordinal': 1, 'name': _('Element')}, 'CSS': {'ordinal': 2, 'name': _('CSS')}, } sample_ann_1 = { 'text': ['Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean placerat condimentum semper. Aliquam hendrerit nisl mauris, nec laoreet orci. Donec rutrum consequat ultricies.', 'Curabitur sollicitudin euismod felis, vitae mollis magna vestibulum id.'], 'note': [_('This is a note appended to the highlight.'), _('And additional comments after a linebreak.')], 'highlightcolor': 'Yellow', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 4', 'location_sort': 0 } sample_ann_2 = { 'text': ['Phasellus sit amet ipsum id velit commodo convallis. In dictum felis non tellus volutpat in tincidunt neque varius. Sed at mauris augue. Vestibulum ligula nunc, ullamcorper id suscipit sed, auctor quis erat. In hac habitasse platea dictumst. Aliquam sit amet nulla dolor, ut tempus libero. In hac habitasse platea dictumst. Etiam consectetur orci vel massa eleifend in vestibulum odio auctor. Praesent orci turpis, aliquet non eleifend sit amet, sollicitudin sit amet augue.'], 'highlightcolor': 'Green', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 12', 'location_sort': 1 } sample_ann_3 = { 'text': ['Morbi massa tellus, laoreet id pretium sed, volutpat in felis.', 'Donec massa nulla, malesuada vitae volutpat quis, accumsan ut tellus.'], 'note': [_('This is a note appended to the highlight.'), _('And additional comments after a linebreak.')], 'highlightcolor': 'Purple', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 53', 'location_sort': 2 } def __init__(self, parent, object_name): self.parent = parent self.prefs = parent.prefs self.elements = self.prefs.get('appearance_css', None) debug_print("AnnotationElementsTable::__init__ - self.elements", self.elements) if not self.elements: self.elements = default_elements debug_print("AnnotationElementsTable::__init__ - self.elements", self.elements) QTableWidget.__init__(self) self.setObjectName(object_name) self.layout = parent.elements_hl.layout() # Add ourselves to the layout sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) #sizePolicy.setVerticalStretch(0) #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) #self.setMaximumSize(QSize(16777215, self.MAXIMUM_TABLE_HEIGHT)) self.setColumnCount(0) self.setRowCount(0) self.layout.addWidget(self) def _init_controls(self): # Add the control set vbl = QVBoxLayout() self.move_element_up_tb = QToolButton() self.move_element_up_tb.setObjectName("move_element_up_tb") self.move_element_up_tb.setToolTip(_('Move element up')) self.move_element_up_tb.setIcon(QIcon(I('arrow-up.png'))) self.move_element_up_tb.clicked.connect(self.move_row_up) vbl.addWidget(self.move_element_up_tb) self.undo_css_tb = QToolButton() self.undo_css_tb.setObjectName("undo_css_tb") self.undo_css_tb.setToolTip(_('Restore CSS to last saved')) self.undo_css_tb.setIcon(QIcon(I('edit-undo.png'))) self.undo_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'undo')) vbl.addWidget(self.undo_css_tb) self.reset_css_tb = QToolButton() self.reset_css_tb.setObjectName("reset_css_tb") self.reset_css_tb.setToolTip(_('Reset CSS to default')) self.reset_css_tb.setIcon(QIcon(I('trash.png'))) self.reset_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'reset')) vbl.addWidget(self.reset_css_tb) self.move_element_down_tb = QToolButton() self.move_element_down_tb.setObjectName("move_element_down_tb") self.move_element_down_tb.setToolTip(_('Move element down')) self.move_element_down_tb.setIcon(QIcon(I('arrow-down.png'))) self.move_element_down_tb.clicked.connect(self.move_row_down) vbl.addWidget(self.move_element_down_tb) self.layout.addLayout(vbl) def _init_table_widget(self): header_labels = [self.COLUMNS[index]['name'] \ for index in sorted(self.COLUMNS.keys(), key=lambda c: self.COLUMNS[c]['ordinal'])] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.verticalHeader().setVisible(False) self.setSortingEnabled(False) # Select single rows self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) def convert_row_to_data(self, row): data = {} data['ordinal'] = row # data['name'] = unicode(self.cellWidget(row, self.COLUMNS['ELEMENT']['ordinal']).text()).strip() data['name'] = unicode(self.cellWidget(row, self.COLUMNS['ELEMENT_NAME']['ordinal']).text()).strip() data['css'] = unicode(self.cellWidget(row, self.COLUMNS['CSS']['ordinal']).toPlainText()).strip() return data def css_edited(self, row): self.select_and_scroll_to_row(row) col = self.COLUMNS['CSS']['ordinal'] widget = self.cellWidget(row, col) css = unicode(widget.toPlainText()) lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) self.resize_row_height(lines, row) # self.prefs.set('appearance_css', self.get_data()) widget.setFocus() self.preview_css() def get_data(self): data_items = [] for row in range(self.rowCount()): data = self.convert_row_to_data(row) data_items.append( {'ordinal': data['ordinal'], 'name': data['name'], # 'translatable_name': translatable_element_names[data['name']], 'css': data['css'], }) return data_items def initialize(self): self._init_table_widget() self._init_controls() self.populate_table() self.resizeColumnsToContents() self.horizontalHeader().setStretchLastSection(True) # Update preview window, select first row self.css_edited(0) def move_row(self, source, dest): self.blockSignals(True) # Save the contents of the destination row saved_data = self.convert_row_to_data(dest) debug_print("Annotations::appearance.py::move_row - saved_data", saved_data) # Remove the destination row self.removeRow(dest) # Insert a new row at the original location self.insertRow(source) # Populate it with the saved data self.populate_table_row(source, saved_data) self.select_and_scroll_to_row(dest) self.blockSignals(False) self.css_edited(dest) def move_row_down(self): src_row = self.currentRow() dest_row = src_row + 1 if dest_row == self.rowCount(): return self.move_row(src_row, dest_row) def move_row_up(self): src_row = self.currentRow() dest_row = src_row - 1 if dest_row < 0: return self.move_row(src_row, dest_row) def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style elements = self.elements if elements and type(elements[0]) is list: elements = elements[0] self.setFocus() elements = sorted(elements, key=lambda k: k['ordinal']) for row, element in enumerate(elements): self.insertRow(row) self.select_and_scroll_to_row(row) self.populate_table_row(row, element) self.selectRow(0) self.setColumnHidden(0, True) def populate_table_row(self, row, data): self.blockSignals(True) self.setCellWidget(row, self.COLUMNS['ELEMENT_NAME']['ordinal'], QLabel(data['name'])) translatable_name = translatable_element_names[data['name']] self.set_element_name_in_row(row, self.COLUMNS['ELEMENT']['ordinal'], translatable_name) self.set_css_in_row(row, self.COLUMNS['CSS']['ordinal'], data['css']) self.blockSignals(False) def preview_css(self): ''' Construct a dummy annotation for preview purposes ''' from calibre_plugins.annotations.annotations import Annotation, Annotations pas = Annotations(None, title=_("Preview")) pas.annotations.append(Annotation(self.sample_ann_1)) pas.annotations.append(Annotation(self.sample_ann_2)) pas.annotations.append(Annotation(self.sample_ann_3)) self.parent.wv.setHtml(pas.to_HTML()) def resize_row_height(self, lines, row): point_size = self.FONT.pointSize() if isosx: height = 30 + (len(lines) - 1) * (point_size + 4) elif iswindows: height = 26 + (len(lines) - 1) * (point_size + 3) elif islinux: height = 30 + (len(lines) - 1) * (point_size + 6) self.verticalHeader().resizeSection(row, height) def select_and_scroll_to_row(self, row): self.setFocus() self.selectRow(row) self.scrollToItem(self.currentItem()) def set_element_name_in_row(self, row, col, name): rule_name = QLabel(" %s " % name) rule_name.setFont(self.FONT) self.setCellWidget(row, col, rule_name) def set_css_in_row(self, row, col, css): # Clean up multi-line css formatting # A single line is 30px tall, subsequent lines add 16px lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) css_content = QPlainTextEdit('\n'.join(lines)) css_content.setFont(self.FONT) css_content.textChanged.connect(partial(self.css_edited, row)) self.setCellWidget(row, col, css_content) self.resize_row_height(lines, row) def undo_reset_button_clicked(self, mode): """ Figure out which element is being reset Reset to last save or default """ debug_print("undo_reset_button_clicked - mode=", mode) row = self.currentRow() data = self.convert_row_to_data(row) debug_print("undo_reset_button_clicked - data=", data) # Get default default_css = None for de in default_elements: if de['name'] == data['name']: default_css = de break # Get last saved last_saved_css = None saved_elements = self.prefs.get('appearance_css', None) last_saved_css = default_css debug_print("undo_reset_button_clicked - saved_elements=", saved_elements) debug_print("undo_reset_button_clicked - last_saved_css=", last_saved_css) if saved_elements: for se in saved_elements: if se['name'] == data['name']: debug_print("undo_reset_button_clicked - se=", se) last_saved_css = se break debug_print("undo_reset_button_clicked - last_saved_css=", last_saved_css) # Restore css if mode == 'reset': self.populate_table_row(row, default_css) elif mode == 'undo': self.populate_table_row(row, last_saved_css) # Refresh the stored data #self.prefs.set('appearance_css', self.get_data()) self.css_edited(row)
def __init__(self, parent): QWidget.__init__(self, parent) self._layout = l = QHBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0, 5, 0, 0) x = QToolButton(self) x.setText(_('Vi&rtual Library')) x.setIcon(QIcon(I('lt.png'))) x.setObjectName("virtual_library") x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) l.addWidget(x) parent.virtual_library = x x = QToolButton(self) x.setIcon(QIcon(I('minus.png'))) x.setObjectName('clear_vl') l.addWidget(x) x.setVisible(False) x.setToolTip(_('Close the Virtual Library')) parent.clear_vl = x x = QLabel(self) x.setObjectName("search_count") l.addWidget(x) parent.search_count = x x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) parent.advanced_search_button = x = QToolButton(self) parent.advanced_search_toggle_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('advanced search toggle', _('Advanced search'), default_keys=("Shift+Ctrl+F", ), action=ac) ac.triggered.connect(x.click) x.setIcon(QIcon(I('search.png'))) l.addWidget(x) x.setToolTip(_("Advanced search")) x = parent.search = SearchBox2(self) x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) x.setObjectName("search") x.setToolTip( _("<p>Search the list of books by title, author, publisher, " "tags, comments, etc.<br><br>Words separated by spaces are ANDed" )) x.setMinimumContentsLength(10) l.addWidget(x) self.search_button = QToolButton() self.search_button.setToolButtonStyle(Qt.ToolButtonTextOnly) self.search_button.setText(_('&Go!')) l.addWidget(self.search_button) self.search_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.search_button.clicked.connect(parent.do_search_button) self.search_button.setToolTip( _('Do Quick Search (you can also press the Enter key)')) x = parent.clear_button = QToolButton(self) x.setIcon(QIcon(I('clear_left.png'))) x.setObjectName("clear_button") l.addWidget(x) x.setToolTip(_("Reset Quick Search")) x = parent.highlight_only_button = QToolButton(self) x.setIcon(QIcon(I('arrow-down.png'))) l.addWidget(x) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) x.setMinimumContentsLength(10) x.setObjectName("saved_search") l.addWidget(x) x = parent.copy_search_button = QToolButton(self) x.setIcon(QIcon(I("search_copy_saved.png"))) x.setObjectName("copy_search_button") l.addWidget(x) x.setToolTip(_("Copy current search text (instead of search name)")) x = parent.save_search_button = RightClickButton(self) x.setIcon(QIcon(I("search_add_saved.png"))) x.setObjectName("save_search_button") l.addWidget(x)
def __init__(self, parent): QWidget.__init__(self, parent) self._layout = l = QHBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0,5,0,0) x = QToolButton(self) x.setText(_('Vi&rtual Library')) x.setIcon(QIcon(I('lt.png'))) x.setObjectName("virtual_library") x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) l.addWidget(x) parent.virtual_library = x x = QToolButton(self) x.setIcon(QIcon(I('minus.png'))) x.setObjectName('clear_vl') l.addWidget(x) x.setVisible(False) x.setToolTip(_('Close the Virtual Library')) parent.clear_vl = x x = QLabel(self) x.setObjectName("search_count") l.addWidget(x) parent.search_count = x x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) parent.advanced_search_button = x = QToolButton(self) parent.advanced_search_toggle_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('advanced search toggle', _('Advanced search'), default_keys=("Shift+Ctrl+F",), action=ac) ac.triggered.connect(x.click) x.setIcon(QIcon(I('search.png'))) l.addWidget(x) x.setToolTip(_("Advanced search")) x = parent.search = SearchBox2(self) x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) x.setObjectName("search") x.setToolTip(_("<p>Search the list of books by title, author, publisher, " "tags, comments, etc.<br><br>Words separated by spaces are ANDed")) x.setMinimumContentsLength(10) l.addWidget(x) self.search_button = QToolButton() self.search_button.setToolButtonStyle(Qt.ToolButtonTextOnly) self.search_button.setText(_('&Go!')) l.addWidget(self.search_button) self.search_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.search_button.clicked.connect(parent.do_search_button) self.search_button.setToolTip( _('Do Quick Search (you can also press the Enter key)')) x = parent.clear_button = QToolButton(self) x.setIcon(QIcon(I('clear_left.png'))) x.setObjectName("clear_button") l.addWidget(x) x.setToolTip(_("Reset Quick Search")) x = parent.highlight_only_button = QToolButton(self) x.setIcon(QIcon(I('arrow-down.png'))) l.addWidget(x) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) x.setMinimumContentsLength(10) x.setObjectName("saved_search") l.addWidget(x) x = parent.copy_search_button = QToolButton(self) x.setIcon(QIcon(I("search_copy_saved.png"))) x.setObjectName("copy_search_button") l.addWidget(x) x.setToolTip(_("Copy current search text (instead of search name)")) x = parent.save_search_button = RightClickButton(self) x.setIcon(QIcon(I("search_add_saved.png"))) x.setObjectName("save_search_button") l.addWidget(x)
class FindAnnotationsDialog(SizePersistedDialog, Logger): GENERIC_STYLE = 'Any style' GENERIC_READER = 'Any reader' def __init__(self, opts): self.matched_ids = set() self.opts = opts self.prefs = opts.prefs super(FindAnnotationsDialog, self).__init__(self.opts.gui, 'find_annotations_dialog') self.setWindowTitle('Find Annotations') self.setWindowIcon(self.opts.icon) self.l = QVBoxLayout(self) self.setLayout(self.l) self.search_criteria_gb = QGroupBox(self) self.search_criteria_gb.setTitle("Search criteria") self.scgl = QGridLayout(self.search_criteria_gb) self.l.addWidget(self.search_criteria_gb) # addWidget(widget, row, col, rowspan, colspan) row = 0 # ~~~~~~~~ Create the Readers comboBox ~~~~~~~~ self.reader_label = QLabel('Reader') self.reader_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.reader_label, row, 0, 1, 1) self.find_annotations_reader_comboBox = QComboBox() self.find_annotations_reader_comboBox.setObjectName('find_annotations_reader_comboBox') self.find_annotations_reader_comboBox.setToolTip('Reader annotations to search for') self.find_annotations_reader_comboBox.addItem(self.GENERIC_READER) racs = ReaderApp.get_reader_app_classes() for ra in sorted(racs.keys()): self.find_annotations_reader_comboBox.addItem(ra) self.scgl.addWidget(self.find_annotations_reader_comboBox, row, 1, 1, 4) row += 1 # ~~~~~~~~ Create the Styles comboBox ~~~~~~~~ self.style_label = QLabel('Style') self.style_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.style_label, row, 0, 1, 1) self.find_annotations_color_comboBox = QComboBox() self.find_annotations_color_comboBox.setObjectName('find_annotations_color_comboBox') self.find_annotations_color_comboBox.setToolTip('Annotation style to search for') self.find_annotations_color_comboBox.addItem(self.GENERIC_STYLE) all_colors = COLOR_MAP.keys() all_colors.remove('Default') for color in sorted(all_colors): self.find_annotations_color_comboBox.addItem(color) self.scgl.addWidget(self.find_annotations_color_comboBox, row, 1, 1, 4) row += 1 # ~~~~~~~~ Create the Text LineEdit control ~~~~~~~~ self.text_label = QLabel('Text') self.text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.text_label, row, 0, 1, 1) self.find_annotations_text_lineEdit = MyLineEdit() self.find_annotations_text_lineEdit.setObjectName('find_annotations_text_lineEdit') self.scgl.addWidget(self.find_annotations_text_lineEdit, row, 1, 1, 3) self.reset_text_tb = QToolButton() self.reset_text_tb.setObjectName('reset_text_tb') self.reset_text_tb.setToolTip('Clear search criteria') self.reset_text_tb.setIcon(QIcon(I('trash.png'))) self.reset_text_tb.clicked.connect(self.clear_text_field) self.scgl.addWidget(self.reset_text_tb, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create the Note LineEdit control ~~~~~~~~ self.note_label = QLabel('Note') self.note_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.note_label, row, 0, 1, 1) self.find_annotations_note_lineEdit = MyLineEdit() self.find_annotations_note_lineEdit.setObjectName('find_annotations_note_lineEdit') self.scgl.addWidget(self.find_annotations_note_lineEdit, row, 1, 1, 3) self.reset_note_tb = QToolButton() self.reset_note_tb.setObjectName('reset_note_tb') self.reset_note_tb.setToolTip('Clear search criteria') self.reset_note_tb.setIcon(QIcon(I('trash.png'))) self.reset_note_tb.clicked.connect(self.clear_note_field) self.scgl.addWidget(self.reset_note_tb, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create the Date range controls ~~~~~~~~ self.date_range_label = QLabel('Date range') self.date_range_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.date_range_label, row, 0, 1, 1) # Date 'From' self.find_annotations_date_from_dateEdit = MyDateEdit(self, datetime(1970,1,1)) self.find_annotations_date_from_dateEdit.setObjectName('find_annotations_date_from_dateEdit') #self.find_annotations_date_from_dateEdit.current_val = datetime(1970,1,1) self.find_annotations_date_from_dateEdit.clear_button.clicked.connect(self.find_annotations_date_from_dateEdit.reset_from_date) self.scgl.addWidget(self.find_annotations_date_from_dateEdit, row, 1, 1, 1) self.scgl.addWidget(self.find_annotations_date_from_dateEdit.clear_button, row, 2, 1, 1) # Date 'To' self.find_annotations_date_to_dateEdit = MyDateEdit(self, datetime.today()) self.find_annotations_date_to_dateEdit.setObjectName('find_annotations_date_to_dateEdit') #self.find_annotations_date_to_dateEdit.current_val = datetime.today() self.find_annotations_date_to_dateEdit.clear_button.clicked.connect(self.find_annotations_date_to_dateEdit.reset_to_date) self.scgl.addWidget(self.find_annotations_date_to_dateEdit, row, 3, 1, 1) self.scgl.addWidget(self.find_annotations_date_to_dateEdit.clear_button, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create a horizontal line ~~~~~~~~ self.hl = QFrame(self) self.hl.setGeometry(QRect(0, 0, 1, 3)) self.hl.setFrameShape(QFrame.HLine) self.hl.setFrameShadow(QFrame.Raised) self.scgl.addWidget(self.hl, row, 0, 1, 5) row += 1 # ~~~~~~~~ Create the results label field ~~~~~~~~ self.result_label = QLabel('<p style="color:red">scanning…</p>') self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setWordWrap(False) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Maximum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.result_label.sizePolicy().hasHeightForWidth()) self.result_label.setSizePolicy(sizePolicy) self.result_label.setMinimumSize(QtCore.QSize(250, 0)) self.scgl.addWidget(self.result_label, row, 0, 1, 5) row += 1 # ~~~~~~~~ Create the ButtonBox ~~~~~~~~ self.dialogButtonBox = QDialogButtonBox(self) self.dialogButtonBox.setOrientation(Qt.Horizontal) if False: self.update_button = QPushButton('Update results') self.update_button.setDefault(True) self.update_button.setVisible(False) self.dialogButtonBox.addButton(self.update_button, QDialogButtonBox.ActionRole) self.cancel_button = self.dialogButtonBox.addButton(self.dialogButtonBox.Cancel) self.find_button = self.dialogButtonBox.addButton(self.dialogButtonBox.Ok) self.find_button.setText('Find Matching Books') self.l.addWidget(self.dialogButtonBox) self.dialogButtonBox.clicked.connect(self.find_annotations_dialog_clicked) # ~~~~~~~~ Add a spacer ~~~~~~~~ self.spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.l.addItem(self.spacerItem) # ~~~~~~~~ Restore previously saved settings ~~~~~~~~ self.restore_settings() # ~~~~~~~~ Declare sizing ~~~~~~~~ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.resize_dialog() # ~~~~~~~~ Connect all signals ~~~~~~~~ self.find_annotations_reader_comboBox.currentIndexChanged.connect(partial(self.update_results, 'reader')) self.find_annotations_color_comboBox.currentIndexChanged.connect(partial(self.update_results, 'color')) self.find_annotations_text_lineEdit.editingFinished.connect(partial(self.update_results, 'text')) self.find_annotations_note_lineEdit.editingFinished.connect(partial(self.update_results, 'note')) # self.connect(self.find_annotations_text_lineEdit, pyqtSignal("return_pressed"), self.return_pressed) self.find_annotations_text_lineEdit.return_pressed.connect(self.return_pressed) # self.connect(self.find_annotations_note_lineEdit, pyqtSignal("return_pressed"), self.return_pressed) self.find_annotations_note_lineEdit.return_pressed.connect(self.return_pressed) # Date range signals connected in inventory_available() # ~~~~~~~~ Allow dialog to render before doing inventory ~~~~~~~~ #field = self.prefs.get('cfg_annotations_destination_field', None) field = get_cc_mapping('annotations', 'field', None) self.annotated_books_scanner = InventoryAnnotatedBooks(self.opts.gui, field, get_date_range=True) self.annotated_books_scanner.signal.connect(self.inventory_available) QTimer.singleShot(1, self.start_inventory_scan) def clear_note_field(self): if str(self.find_annotations_note_lineEdit.text()) > '': self.find_annotations_note_lineEdit.setText('') self.update_results('clear_note_field') def clear_text_field(self): if str(self.find_annotations_text_lineEdit.text()) > '': self.find_annotations_text_lineEdit.setText('') self.update_results('clear_text_field') def find_annotations_dialog_clicked(self, button): if self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.AcceptRole: self.save_settings() self.accept() elif self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.RejectRole: self.close() def inventory_available(self): ''' Update the Date range widgets with the rounded oldest, newest dates Don't connect date signals until date range available ''' self._log_location() # Reset the date range based on available annotations oldest = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.oldest_annotation)) oldest_day = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.oldest_annotation).replace(hour=0, minute=0, second=0)) newest = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.newest_annotation)) newest_day = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.newest_annotation).replace(hour=23, minute=59, second=59)) # Set 'From' date limits to inventory values self.find_annotations_date_from_dateEdit.setMinimumDateTime(oldest_day) self.find_annotations_date_from_dateEdit.current_val = oldest self.find_annotations_date_from_dateEdit.setMaximumDateTime(newest_day) # Set 'To' date limits to inventory values self.find_annotations_date_to_dateEdit.setMinimumDateTime(oldest_day) self.find_annotations_date_to_dateEdit.current_val = newest_day self.find_annotations_date_to_dateEdit.setMaximumDateTime(newest_day) # Connect the signals for date range changes self.find_annotations_date_from_dateEdit.dateTimeChanged.connect(partial(self.update_results, 'from_date')) self.find_annotations_date_to_dateEdit.dateTimeChanged.connect(partial(self.update_results, 'to_date')) self.update_results('inventory_available') def restore_settings(self): self.blockSignals(True) ra = self.prefs.get('find_annotations_reader_comboBox', self.GENERIC_READER) ra_index = self.find_annotations_reader_comboBox.findText(ra) self.find_annotations_reader_comboBox.setCurrentIndex(ra_index) color = self.prefs.get('find_annotations_color_comboBox', self.GENERIC_STYLE) color_index = self.find_annotations_color_comboBox.findText(color) self.find_annotations_color_comboBox.setCurrentIndex(color_index) text = self.prefs.get('find_annotations_text_lineEdit', '') self.find_annotations_text_lineEdit.setText(text) note = self.prefs.get('find_annotations_note_lineEdit', '') self.find_annotations_note_lineEdit.setText(note) if False: from_date = self.prefs.get('find_annotations_date_from_dateEdit', datetime(1970,1,1)) self.find_annotations_date_from_dateEdit.current_val = from_date to_date = self.prefs.get('find_annotations_date_to_dateEdit', datetime.today()) self.find_annotations_date_to_dateEdit.current_val = to_date self.blockSignals(False) def return_pressed(self): self.update_results("return_pressed") def save_settings(self): ra = str(self.find_annotations_reader_comboBox.currentText()) self.prefs.set('find_annotations_reader_comboBox', ra) color = str(self.find_annotations_color_comboBox.currentText()) self.prefs.set('find_annotations_color_comboBox', color) text = str(self.find_annotations_text_lineEdit.text()) self.prefs.set('find_annotations_text_lineEdit', text) note = str(self.find_annotations_note_lineEdit.text()) self.prefs.set('find_annotations_note_lineEdit', note) if False: from_date = self.find_annotations_date_from_dateEdit.current_val self.prefs.set('find_annotations_date_from_dateEdit', from_date) to_date = self.find_annotations_date_to_dateEdit.current_val self.prefs.set('find_annotations_date_to_dateEdit', to_date) def start_inventory_scan(self): self._log_location() self.annotated_books_scanner.start() def update_results(self, trigger): #self._log_location(trigger) reader_to_match = str(self.find_annotations_reader_comboBox.currentText()) color_to_match = str(self.find_annotations_color_comboBox.currentText()) text_to_match = str(self.find_annotations_text_lineEdit.text()) note_to_match = str(self.find_annotations_note_lineEdit.text()) from_date = self.find_annotations_date_from_dateEdit.dateTime().toTime_t() to_date = self.find_annotations_date_to_dateEdit.dateTime().toTime_t() annotation_map = self.annotated_books_scanner.annotation_map #field = self.prefs.get("cfg_annotations_destination_field", None) field = get_cc_mapping('annotations', 'field', None) db = self.opts.gui.current_db matched_titles = [] self.matched_ids = set() for cid in annotation_map: mi = db.get_metadata(cid, index_is_id=True) soup = None if field == 'Comments': if mi.comments: soup = BeautifulSoup(mi.comments) else: if mi.get_user_metadata(field, False)['#value#'] is not None: soup = BeautifulSoup(mi.get_user_metadata(field, False)['#value#']) if soup: uas = soup.findAll('div', 'annotation') for ua in uas: # Are we already logged? if cid in self.matched_ids: continue # Check reader if reader_to_match != self.GENERIC_READER: this_reader = ua['reader'] if this_reader != reader_to_match: continue # Check color if color_to_match != self.GENERIC_STYLE: this_color = ua.find('table')['color'] if this_color != color_to_match: continue # Check date range, allow for mangled timestamp try: timestamp = float(ua.find('td', 'timestamp')['uts']) if timestamp < from_date or timestamp > to_date: continue except: continue highlight_text = '' try: pels = ua.findAll('p', 'highlight') for pel in pels: highlight_text += pel.string + '\n' except: pass if text_to_match > '': if not re.search(text_to_match, highlight_text, flags=re.IGNORECASE): continue note_text = '' try: nels = ua.findAll('p', 'note') for nel in nels: note_text += nel.string + '\n' except: pass if note_to_match > '': if not re.search(note_to_match, note_text, flags=re.IGNORECASE): continue # If we made it this far, add the id to matched_ids self.matched_ids.add(cid) matched_titles.append(mi.title) # Update the results box matched_titles.sort() if len(annotation_map): if len(matched_titles): first_match = ("<i>%s</i>" % matched_titles[0]) if len(matched_titles) == 1: results = first_match else: results = first_match + (" and %d more." % (len(matched_titles) - 1)) self.result_label.setText('<p style="color:blue">{0}</p>'.format(results)) else: self.result_label.setText('<p style="color:red">no matches</p>') else: self.result_label.setText('<p style="color:red">no annotated books in library</p>') self.resize_dialog()
def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.Shape.NoFrame) self.setObjectName('search_bar') self._layout = l = QHBoxLayout(self) l.setContentsMargins(0, 4, 0, 4) x = parent.virtual_library = QToolButton(self) x.setCursor(Qt.CursorShape.PointingHandCursor) x.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) x.setText(_('Virtual library')) x.setAutoRaise(True) x.setIcon(QIcon(I('vl.png'))) x.setObjectName("virtual_library") x.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) l.addWidget(x) x = QToolButton(self) x.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) x.setAutoRaise(True) x.setIcon(QIcon(I('minus.png'))) x.setObjectName('clear_vl') l.addWidget(x) x.setVisible(False) x.setToolTip(_('Close the Virtual library')) parent.clear_vl = x self.vl_sep = QFrame(self) self.vl_sep.setFrameStyle(QFrame.Shape.VLine | QFrame.Shadow.Sunken) l.addWidget(self.vl_sep) parent.sort_sep = QFrame(self) parent.sort_sep.setFrameStyle(QFrame.Shape.VLine | QFrame.Shadow.Sunken) parent.sort_sep.setVisible(False) parent.sort_button = self.sort_button = sb = QToolButton(self) sb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) sb.setToolTip(_('Change how the displayed books are sorted')) sb.setCursor(Qt.CursorShape.PointingHandCursor) sb.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) sb.setAutoRaise(True) sb.setText(_('Sort')) sb.setIcon(QIcon(I('sort.png'))) sb.setMenu(QMenu(sb)) sb.menu().aboutToShow.connect(self.populate_sort_menu) sb.setVisible(False) l.addWidget(sb) l.addWidget(parent.sort_sep) x = parent.search = SearchBox2(self, as_url=search_as_url) x.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) x.setObjectName("search") x.setToolTip(_("<p>Search the list of books by title, author, publisher, " "tags, comments, etc.<br><br>Words separated by spaces are ANDed")) x.setMinimumContentsLength(10) l.addWidget(x) parent.advanced_search_toggle_action = ac = parent.search.add_action('gear.png', QLineEdit.ActionPosition.LeadingPosition) parent.addAction(ac) ac.setToolTip(_('Advanced search')) parent.keyboard.register_shortcut('advanced search toggle', _('Advanced search'), default_keys=("Shift+Ctrl+F",), action=ac) self.search_button = QToolButton() self.search_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly) self.search_button.setIcon(QIcon(I('search.png'))) self.search_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.search_button.setText(_('Search')) self.search_button.setAutoRaise(True) self.search_button.setCursor(Qt.CursorShape.PointingHandCursor) l.addWidget(self.search_button) self.search_button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) self.search_button.clicked.connect(parent.do_search_button) self.search_button.setToolTip( _('Do quick search (you can also press the Enter key)')) x = parent.highlight_only_button = QToolButton(self) x.setAutoRaise(True) x.setText(_('Highlight')) x.setCursor(Qt.CursorShape.PointingHandCursor) x.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) x.setIcon(QIcon(I('arrow-down.png'))) l.addWidget(x) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) x.setMinimumContentsLength(10) x.setObjectName("saved_search") l.addWidget(x) x.setVisible(tweaks['show_saved_search_box']) x = parent.copy_search_button = QToolButton(self) x.setAutoRaise(True) x.setCursor(Qt.CursorShape.PointingHandCursor) x.setIcon(QIcon(I("search_copy_saved.png"))) x.setObjectName("copy_search_button") l.addWidget(x) x.setToolTip(_("Copy current search text (instead of search name)")) x.setVisible(tweaks['show_saved_search_box']) x = parent.save_search_button = RightClickButton(self) x.setAutoRaise(True) x.setCursor(Qt.CursorShape.PointingHandCursor) x.setIcon(QIcon(I("search_add_saved.png"))) x.setObjectName("save_search_button") l.addWidget(x) x.setVisible(tweaks['show_saved_search_box']) x = parent.add_saved_search_button = RightClickButton(self) x.setToolTip(_( 'Use an existing Saved search or create a new one' )) x.setText(_('Saved search')) x.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) x.setCursor(Qt.CursorShape.PointingHandCursor) x.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) x.setAutoRaise(True) x.setIcon(QIcon(I("bookmarks.png"))) l.addWidget(x) x.setVisible(not tweaks['show_saved_search_box'])
class FindAnnotationsDialog(SizePersistedDialog, Logger): GENERIC_STYLE = 'Any style' GENERIC_READER = 'Any reader' def __init__(self, opts): self.matched_ids = set() self.opts = opts self.prefs = opts.prefs super(FindAnnotationsDialog, self).__init__(self.opts.gui, 'find_annotations_dialog') self.setWindowTitle(_('Find Annotations')) self.setWindowIcon(self.opts.icon) self.l = QVBoxLayout(self) self.setLayout(self.l) self.search_criteria_gb = QGroupBox(self) self.search_criteria_gb.setTitle(_("Search criteria")) self.scgl = QGridLayout(self.search_criteria_gb) self.l.addWidget(self.search_criteria_gb) # addWidget(widget, row, col, rowspan, colspan) row = 0 # ~~~~~~~~ Create the Readers comboBox ~~~~~~~~ self.reader_label = QLabel(_('Reader')) self.reader_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.reader_label, row, 0, 1, 1) self.find_annotations_reader_comboBox = QComboBox() self.find_annotations_reader_comboBox.setObjectName('find_annotations_reader_comboBox') self.find_annotations_reader_comboBox.setToolTip(_('Reader annotations to search for')) self.find_annotations_reader_comboBox.addItem(self.GENERIC_READER) racs = ReaderApp.get_reader_app_classes() for ra in sorted(racs.keys()): self.find_annotations_reader_comboBox.addItem(ra) self.scgl.addWidget(self.find_annotations_reader_comboBox, row, 1, 1, 4) row += 1 # ~~~~~~~~ Create the Styles comboBox ~~~~~~~~ self.style_label = QLabel(_('Style')) self.style_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.style_label, row, 0, 1, 1) self.find_annotations_color_comboBox = QComboBox() self.find_annotations_color_comboBox.setObjectName('find_annotations_color_comboBox') self.find_annotations_color_comboBox.setToolTip(_('Annotation style to search for')) self.find_annotations_color_comboBox.addItem(self.GENERIC_STYLE) all_colors = COLOR_MAP.keys() all_colors.remove('Default') for color in sorted(all_colors): self.find_annotations_color_comboBox.addItem(color) self.scgl.addWidget(self.find_annotations_color_comboBox, row, 1, 1, 4) row += 1 # ~~~~~~~~ Create the Text LineEdit control ~~~~~~~~ self.text_label = QLabel(_('Text')) self.text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.text_label, row, 0, 1, 1) self.find_annotations_text_lineEdit = MyLineEdit() self.find_annotations_text_lineEdit.setObjectName('find_annotations_text_lineEdit') self.scgl.addWidget(self.find_annotations_text_lineEdit, row, 1, 1, 3) self.reset_text_tb = QToolButton() self.reset_text_tb.setObjectName('reset_text_tb') self.reset_text_tb.setToolTip(_('Clear search criteria')) self.reset_text_tb.setIcon(QIcon(I('trash.png'))) self.reset_text_tb.clicked.connect(self.clear_text_field) self.scgl.addWidget(self.reset_text_tb, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create the Note LineEdit control ~~~~~~~~ self.note_label = QLabel(_('Note')) self.note_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.note_label, row, 0, 1, 1) self.find_annotations_note_lineEdit = MyLineEdit() self.find_annotations_note_lineEdit.setObjectName('find_annotations_note_lineEdit') self.scgl.addWidget(self.find_annotations_note_lineEdit, row, 1, 1, 3) self.reset_note_tb = QToolButton() self.reset_note_tb.setObjectName('reset_note_tb') self.reset_note_tb.setToolTip(_('Clear search criteria')) self.reset_note_tb.setIcon(QIcon(I('trash.png'))) self.reset_note_tb.clicked.connect(self.clear_note_field) self.scgl.addWidget(self.reset_note_tb, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create the Date range controls ~~~~~~~~ self.date_range_label = QLabel(_('Date range')) self.date_range_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.scgl.addWidget(self.date_range_label, row, 0, 1, 1) # Date 'From' self.find_annotations_date_from_dateEdit = MyDateEdit(self, datetime(1970,1,1)) self.find_annotations_date_from_dateEdit.setObjectName('find_annotations_date_from_dateEdit') #self.find_annotations_date_from_dateEdit.current_val = datetime(1970,1,1) self.find_annotations_date_from_dateEdit.clear_button.clicked.connect(self.find_annotations_date_from_dateEdit.reset_from_date) self.scgl.addWidget(self.find_annotations_date_from_dateEdit, row, 1, 1, 1) self.scgl.addWidget(self.find_annotations_date_from_dateEdit.clear_button, row, 2, 1, 1) # Date 'To' self.find_annotations_date_to_dateEdit = MyDateEdit(self, datetime.today()) self.find_annotations_date_to_dateEdit.setObjectName('find_annotations_date_to_dateEdit') #self.find_annotations_date_to_dateEdit.current_val = datetime.today() self.find_annotations_date_to_dateEdit.clear_button.clicked.connect(self.find_annotations_date_to_dateEdit.reset_to_date) self.scgl.addWidget(self.find_annotations_date_to_dateEdit, row, 3, 1, 1) self.scgl.addWidget(self.find_annotations_date_to_dateEdit.clear_button, row, 4, 1, 1) row += 1 # ~~~~~~~~ Create a horizontal line ~~~~~~~~ self.hl = QFrame(self) self.hl.setGeometry(QRect(0, 0, 1, 3)) self.hl.setFrameShape(QFrame.HLine) self.hl.setFrameShadow(QFrame.Raised) self.scgl.addWidget(self.hl, row, 0, 1, 5) row += 1 # ~~~~~~~~ Create the results label field ~~~~~~~~ self.result_label = QLabel('<p style="color:red">{0}</p>'.format(_('scanning…'))) self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setWordWrap(False) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Maximum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.result_label.sizePolicy().hasHeightForWidth()) self.result_label.setSizePolicy(sizePolicy) self.result_label.setMinimumSize(QtCore.QSize(250, 0)) self.scgl.addWidget(self.result_label, row, 0, 1, 5) row += 1 # ~~~~~~~~ Create the ButtonBox ~~~~~~~~ self.dialogButtonBox = QDialogButtonBox(self) self.dialogButtonBox.setOrientation(Qt.Horizontal) if False: self.update_button = QPushButton(_('Update results')) self.update_button.setDefault(True) self.update_button.setVisible(False) self.dialogButtonBox.addButton(self.update_button, QDialogButtonBox.ActionRole) self.cancel_button = self.dialogButtonBox.addButton(self.dialogButtonBox.Cancel) self.find_button = self.dialogButtonBox.addButton(self.dialogButtonBox.Ok) self.find_button.setText(_('Find Matching Books')) self.l.addWidget(self.dialogButtonBox) self.dialogButtonBox.clicked.connect(self.find_annotations_dialog_clicked) # ~~~~~~~~ Add a spacer ~~~~~~~~ self.spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.l.addItem(self.spacerItem) # ~~~~~~~~ Restore previously saved settings ~~~~~~~~ self.restore_settings() # ~~~~~~~~ Declare sizing ~~~~~~~~ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.resize_dialog() # ~~~~~~~~ Connect all signals ~~~~~~~~ self.find_annotations_reader_comboBox.currentIndexChanged.connect(partial(self.update_results, 'reader')) self.find_annotations_color_comboBox.currentIndexChanged.connect(partial(self.update_results, 'color')) self.find_annotations_text_lineEdit.editingFinished.connect(partial(self.update_results, 'text')) self.find_annotations_note_lineEdit.editingFinished.connect(partial(self.update_results, 'note')) # self.connect(self.find_annotations_text_lineEdit, pyqtSignal("return_pressed"), self.return_pressed) self.find_annotations_text_lineEdit.return_pressed.connect(self.return_pressed) # self.connect(self.find_annotations_note_lineEdit, pyqtSignal("return_pressed"), self.return_pressed) self.find_annotations_note_lineEdit.return_pressed.connect(self.return_pressed) # Date range signals connected in inventory_available() # ~~~~~~~~ Allow dialog to render before doing inventory ~~~~~~~~ #field = self.prefs.get('cfg_annotations_destination_field', None) field = get_cc_mapping('annotations', 'field', None) self.annotated_books_scanner = InventoryAnnotatedBooks(self.opts.gui, field, get_date_range=True) self.annotated_books_scanner.signal.connect(self.inventory_available) QTimer.singleShot(1, self.start_inventory_scan) def clear_note_field(self): if str(self.find_annotations_note_lineEdit.text()) > '': self.find_annotations_note_lineEdit.setText('') self.update_results('clear_note_field') def clear_text_field(self): if str(self.find_annotations_text_lineEdit.text()) > '': self.find_annotations_text_lineEdit.setText('') self.update_results('clear_text_field') def find_annotations_dialog_clicked(self, button): if self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.AcceptRole: self.save_settings() self.accept() elif self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.RejectRole: self.close() def inventory_available(self): ''' Update the Date range widgets with the rounded oldest, newest dates Don't connect date signals until date range available ''' self._log_location() # Reset the date range based on available annotations oldest = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.oldest_annotation)) oldest_day = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.oldest_annotation).replace(hour=0, minute=0, second=0)) newest = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.newest_annotation)) newest_day = QDateTime(datetime.fromtimestamp(self.annotated_books_scanner.newest_annotation).replace(hour=23, minute=59, second=59)) # Set 'From' date limits to inventory values self.find_annotations_date_from_dateEdit.setMinimumDateTime(oldest_day) self.find_annotations_date_from_dateEdit.current_val = oldest self.find_annotations_date_from_dateEdit.setMaximumDateTime(newest_day) # Set 'To' date limits to inventory values self.find_annotations_date_to_dateEdit.setMinimumDateTime(oldest_day) self.find_annotations_date_to_dateEdit.current_val = newest_day self.find_annotations_date_to_dateEdit.setMaximumDateTime(newest_day) # Connect the signals for date range changes self.find_annotations_date_from_dateEdit.dateTimeChanged.connect(partial(self.update_results, 'from_date')) self.find_annotations_date_to_dateEdit.dateTimeChanged.connect(partial(self.update_results, 'to_date')) self.update_results('inventory_available') def restore_settings(self): self.blockSignals(True) ra = self.prefs.get('find_annotations_reader_comboBox', self.GENERIC_READER) ra_index = self.find_annotations_reader_comboBox.findText(ra) self.find_annotations_reader_comboBox.setCurrentIndex(ra_index) color = self.prefs.get('find_annotations_color_comboBox', self.GENERIC_STYLE) color_index = self.find_annotations_color_comboBox.findText(color) self.find_annotations_color_comboBox.setCurrentIndex(color_index) text = self.prefs.get('find_annotations_text_lineEdit', '') self.find_annotations_text_lineEdit.setText(text) note = self.prefs.get('find_annotations_note_lineEdit', '') self.find_annotations_note_lineEdit.setText(note) if False: from_date = self.prefs.get('find_annotations_date_from_dateEdit', datetime(1970,1,1)) self.find_annotations_date_from_dateEdit.current_val = from_date to_date = self.prefs.get('find_annotations_date_to_dateEdit', datetime.today()) self.find_annotations_date_to_dateEdit.current_val = to_date self.blockSignals(False) def return_pressed(self): self.update_results("return_pressed") def save_settings(self): ra = str(self.find_annotations_reader_comboBox.currentText()) self.prefs.set('find_annotations_reader_comboBox', ra) color = str(self.find_annotations_color_comboBox.currentText()) self.prefs.set('find_annotations_color_comboBox', color) text = str(self.find_annotations_text_lineEdit.text()) self.prefs.set('find_annotations_text_lineEdit', text) note = str(self.find_annotations_note_lineEdit.text()) self.prefs.set('find_annotations_note_lineEdit', note) if False: from_date = self.find_annotations_date_from_dateEdit.current_val self.prefs.set('find_annotations_date_from_dateEdit', from_date) to_date = self.find_annotations_date_to_dateEdit.current_val self.prefs.set('find_annotations_date_to_dateEdit', to_date) def start_inventory_scan(self): self._log_location() self.annotated_books_scanner.start() def update_results(self, trigger): #self._log_location(trigger) reader_to_match = str(self.find_annotations_reader_comboBox.currentText()) color_to_match = str(self.find_annotations_color_comboBox.currentText()) text_to_match = str(self.find_annotations_text_lineEdit.text()) note_to_match = str(self.find_annotations_note_lineEdit.text()) from_date = self.find_annotations_date_from_dateEdit.dateTime().toTime_t() to_date = self.find_annotations_date_to_dateEdit.dateTime().toTime_t() annotation_map = self.annotated_books_scanner.annotation_map #field = self.prefs.get("cfg_annotations_destination_field", None) field = get_cc_mapping('annotations', 'field', None) db = self.opts.gui.current_db matched_titles = [] self.matched_ids = set() for cid in annotation_map: mi = db.get_metadata(cid, index_is_id=True) soup = None if field == 'Comments': if mi.comments: soup = BeautifulSoup(mi.comments) else: if mi.get_user_metadata(field, False)['#value#'] is not None: soup = BeautifulSoup(mi.get_user_metadata(field, False)['#value#']) if soup: uas = soup.findAll('div', 'annotation') for ua in uas: # Are we already logged? if cid in self.matched_ids: continue # Check reader if reader_to_match != self.GENERIC_READER: this_reader = ua['reader'] if this_reader != reader_to_match: continue # Check color if color_to_match != self.GENERIC_STYLE: this_color = ua.find('table')['color'] if this_color != color_to_match: continue # Check date range, allow for mangled timestamp try: timestamp = float(ua.find('td', 'timestamp')['uts']) if timestamp < from_date or timestamp > to_date: continue except: continue highlight_text = '' try: pels = ua.findAll('p', 'highlight') highlight_text = '\n'.join([p.string for p in pels]) except: pass if text_to_match > '': if not re.search(text_to_match, highlight_text, flags=re.IGNORECASE): continue note_text = '' try: nels = ua.findAll('p', 'note') note_text = '\n'.join([n.string for n in nels]) except: pass if note_to_match > '': if not re.search(note_to_match, note_text, flags=re.IGNORECASE): continue # If we made it this far, add the id to matched_ids self.matched_ids.add(cid) matched_titles.append(mi.title) # Update the results box matched_titles.sort() if len(annotation_map): if len(matched_titles): first_match = ("<i>%s</i>" % matched_titles[0]) if len(matched_titles) == 1: results = first_match else: results = first_match + (_(" and {0} more.").format(len(matched_titles) - 1)) self.result_label.setText('<p style="color:blue">{0}</p>'.format(results)) else: self.result_label.setText('<p style="color:red">{0}</p>'.format(_('no matches'))) else: self.result_label.setText('<p style="color:red">{0}</p>'.format(_('no annotated books in library'))) self.resize_dialog()
def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame) self.setObjectName('search_bar') self._layout = l = QHBoxLayout(self) l.setContentsMargins(0, 4, 0, 4) x = parent.virtual_library = QToolButton(self) x.setCursor(Qt.PointingHandCursor) x.setPopupMode(x.InstantPopup) x.setText(_('Virtual library')) x.setAutoRaise(True) x.setIcon(QIcon(I('lt.png'))) x.setObjectName("virtual_library") x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) l.addWidget(x) x = QToolButton(self) x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) x.setAutoRaise(True) x.setIcon(QIcon(I('minus.png'))) x.setObjectName('clear_vl') l.addWidget(x) x.setVisible(False) x.setToolTip(_('Close the Virtual library')) parent.clear_vl = x self.vl_sep = QFrame(self) self.vl_sep.setFrameStyle(QFrame.VLine | QFrame.Sunken) l.addWidget(self.vl_sep) parent.sort_sep = QFrame(self) parent.sort_sep.setFrameStyle(QFrame.VLine | QFrame.Sunken) parent.sort_sep.setVisible(False) parent.sort_button = self.sort_button = sb = QToolButton(self) sb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) sb.setToolTip(_('Change how the displayed books are sorted')) sb.setCursor(Qt.PointingHandCursor) sb.setPopupMode(QToolButton.InstantPopup) sb.setAutoRaise(True) sb.setText(_('Sort')) sb.setIcon(QIcon(I('sort.png'))) sb.setMenu(QMenu()) sb.menu().aboutToShow.connect(self.populate_sort_menu) sb.setVisible(False) l.addWidget(sb) l.addWidget(parent.sort_sep) x = parent.search = SearchBox2(self) x.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) x.setObjectName("search") x.setToolTip(_("<p>Search the list of books by title, author, publisher, " "tags, comments, etc.<br><br>Words separated by spaces are ANDed")) x.setMinimumContentsLength(10) l.addWidget(x) parent.advanced_search_toggle_action = ac = parent.search.add_action('gear.png', QLineEdit.LeadingPosition) parent.addAction(ac) ac.setToolTip(_('Advanced search')) parent.keyboard.register_shortcut('advanced search toggle', _('Advanced search'), default_keys=("Shift+Ctrl+F",), action=ac) self.search_button = QToolButton() self.search_button.setToolButtonStyle(Qt.ToolButtonTextOnly) self.search_button.setIcon(QIcon(I('search.png'))) self.search_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.search_button.setText(_('Search')) self.search_button.setAutoRaise(True) self.search_button.setCursor(Qt.PointingHandCursor) l.addWidget(self.search_button) self.search_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.search_button.clicked.connect(parent.do_search_button) self.search_button.setToolTip( _('Do Quick Search (you can also press the Enter key)')) x = parent.highlight_only_button = QToolButton(self) x.setAutoRaise(True) x.setText(_('Highlight')) x.setCursor(Qt.PointingHandCursor) x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) x.setIcon(QIcon(I('arrow-down.png'))) l.addWidget(x) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) x.setMinimumContentsLength(10) x.setObjectName("saved_search") l.addWidget(x) x.setVisible(tweaks['show_saved_search_box']) x = parent.copy_search_button = QToolButton(self) x.setAutoRaise(True) x.setCursor(Qt.PointingHandCursor) x.setIcon(QIcon(I("search_copy_saved.png"))) x.setObjectName("copy_search_button") l.addWidget(x) x.setToolTip(_("Copy current search text (instead of search name)")) x.setVisible(tweaks['show_saved_search_box']) x = parent.save_search_button = RightClickButton(self) x.setAutoRaise(True) x.setCursor(Qt.PointingHandCursor) x.setIcon(QIcon(I("search_add_saved.png"))) x.setObjectName("save_search_button") l.addWidget(x) x.setVisible(tweaks['show_saved_search_box']) x = parent.add_saved_search_button = RightClickButton(self) x.setToolTip(_( 'Use an existing Saved search or create a new one' )) x.setText(_('Saved search')) x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) x.setCursor(Qt.PointingHandCursor) x.setPopupMode(x.InstantPopup) x.setAutoRaise(True) x.setIcon(QIcon(I("bookmarks.png"))) l.addWidget(x) x.setVisible(not tweaks['show_saved_search_box'])
class AnnotationElementsTable(QTableWidget): ''' QTableWidget managing CSS rules ''' DEBUG = True #MAXIMUM_TABLE_HEIGHT = 113 ELEMENT_FIELD_WIDTH = 250 if isosx: FONT = QFont('Monaco', 11) elif iswindows: FONT = QFont('Lucida Console', 9) elif islinux: FONT = QFont('Monospace', 9) FONT.setStyleHint(QFont.TypeWriter) COLUMNS = { 'ELEMENT': {'ordinal': 0, 'name': 'Element'}, 'CSS': {'ordinal': 1, 'name': 'CSS'}, } # Sample content for preview sample_ann_1 = { 'text': [ ("What really knocks me out is a book that, when you're all done reading it, " "you wish the author that wrote it was a terrific friend of yours and " "you could call him up on the phone whenever you felt like it. " "That doesn't happen much, though.")], 'note': ['— J.D. Salinger'], 'highlightcolor': 'Yellow', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 4', 'location_sort': 0 } sample_ann_2 = { 'text': [ ("Literature is a luxury; fiction is a necessity.")], 'note': ['— G.K. Chesterton'], 'highlightcolor': 'Pink', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 12', 'location_sort': 1 } sample_ann_3 = { 'text': [ ("There is no surer foundation for a beautiful friendship " "than a mutual taste in literature.")], 'note': ['— P.G. Wodehouse'], 'highlightcolor': 'Purple', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 53', 'location_sort': 2 } sample_book_notes = [ "Create book notes from Marvin’s Library view by swiping left on a cover, " "then touching <b>Notes</b>.<br/>" "Create book notes from Marvin’s Home screen by long-touching a cover, then " "touching <b>Notes</b>." ] sample_bookmark_notes = { "1": {'color': 'bookmark_red', 'location': 'Chapter 1', 'note': ('Create bookmark notes from Marvin’s <b>Bookmarks</b> screen. ' 'Access the Table of Contents screen by tapping the book icon ' 'in the top toolbar of the Book screen, then tapping <b>Bookmarks</b> ' 'in the bottom toolbar. ' 'Swipe left on a bookmark to edit.')}, "2": {'color': 'bookmark_green', 'location': 'Chapter 2', 'note': ('Enable multi-color bookmarks from Marvin’s ' '<b>Options | More</b> screen.')}, } def __init__(self, parent, object_name): self.parent = parent self.prefs = parent.prefs self.elements = self.prefs.get('appearance_css', None) if not self.elements: self.elements = default_elements QTableWidget.__init__(self) self.setObjectName(object_name) self.layout = parent.elements_hl.layout() # Add ourselves to the layout sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) #sizePolicy.setVerticalStretch(0) #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) #self.setMaximumSize(QSize(16777215, self.MAXIMUM_TABLE_HEIGHT)) self.setColumnCount(0) self.setRowCount(0) self.layout.addWidget(self) def _init_controls(self): # Add the control set vbl = QVBoxLayout() self.move_element_up_tb = QToolButton() self.move_element_up_tb.setObjectName("move_element_up_tb") self.move_element_up_tb.setToolTip('Move element up') self.move_element_up_tb.setIcon(QIcon(I('arrow-up.png'))) self.move_element_up_tb.clicked.connect(self.move_row_up) vbl.addWidget(self.move_element_up_tb) self.undo_css_tb = QToolButton() self.undo_css_tb.setObjectName("undo_css_tb") self.undo_css_tb.setToolTip('Restore CSS to last saved') self.undo_css_tb.setIcon(QIcon(I('edit-undo.png'))) self.undo_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'undo')) vbl.addWidget(self.undo_css_tb) self.reset_css_tb = QToolButton() self.reset_css_tb.setObjectName("reset_css_tb") self.reset_css_tb.setToolTip('Reset CSS to default') self.reset_css_tb.setIcon(QIcon(I('trash.png'))) self.reset_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'reset')) vbl.addWidget(self.reset_css_tb) self.move_element_down_tb = QToolButton() self.move_element_down_tb.setObjectName("move_element_down_tb") self.move_element_down_tb.setToolTip('Move element down') self.move_element_down_tb.setIcon(QIcon(I('arrow-down.png'))) self.move_element_down_tb.clicked.connect(self.move_row_down) vbl.addWidget(self.move_element_down_tb) self.layout.addLayout(vbl) def _init_table_widget(self): header_labels = [self.COLUMNS[index]['name'] \ for index in sorted(self.COLUMNS.keys(), key=lambda c: self.COLUMNS[c]['ordinal'])] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.verticalHeader().setVisible(False) self.setSortingEnabled(False) # Select single rows self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) def convert_row_to_data(self, row): data = {} data['ordinal'] = row data['name'] = unicode(self.cellWidget(row, self.COLUMNS['ELEMENT']['ordinal']).text()).strip() data['css'] = unicode(self.cellWidget(row, self.COLUMNS['CSS']['ordinal']).toPlainText()).strip() return data def css_edited(self, row): self.select_and_scroll_to_row(row) col = self.COLUMNS['CSS']['ordinal'] widget = self.cellWidget(row, col) css = unicode(widget.toPlainText()) lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) self.resize_row_height(lines, row) self.prefs.set('appearance_css', self.get_data()) widget.setFocus() self.preview_css() def get_data(self): data_items = [] for row in range(self.rowCount()): data = self.convert_row_to_data(row) data_items.append( {'ordinal': data['ordinal'], 'name': data['name'], 'css': data['css'], }) return data_items def initialize(self): self._init_table_widget() self._init_controls() self.populate_table() self.resizeColumnsToContents() self.horizontalHeader().setStretchLastSection(True) # Update preview window, select first row self.css_edited(0) def move_row(self, source, dest): self.blockSignals(True) # Save the contents of the destination row saved_data = self.convert_row_to_data(dest) # Remove the destination row self.removeRow(dest) # Insert a new row at the original location self.insertRow(source) # Populate it with the saved data self.populate_table_row(source, saved_data) self.select_and_scroll_to_row(dest) self.blockSignals(False) self.css_edited(dest) def move_row_down(self): src_row = self.currentRow() dest_row = src_row + 1 if dest_row == self.rowCount(): return self.move_row(src_row, dest_row) def move_row_up(self): src_row = self.currentRow() dest_row = src_row - 1 if dest_row < 0: return self.move_row(src_row, dest_row) def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style elements = self.elements if elements and type(elements[0]) is list: elements = elements[0] self.setFocus() elements = sorted(elements, key=lambda k: k['ordinal']) for row, element in enumerate(elements): self.insertRow(row) self.select_and_scroll_to_row(row) self.populate_table_row(row, element) self.selectRow(0) def populate_table_row(self, row, data): self.blockSignals(True) self.set_element_name_in_row(row, self.COLUMNS['ELEMENT']['ordinal'], data['name']) self.set_css_in_row(row, self.COLUMNS['CSS']['ordinal'], data['css']) self.blockSignals(False) def preview_css(self): ''' Construct a dummy set of notes and annotation for preview purposes Modeled after book_status:_get_formatted_annotations() ''' from calibre_plugins.marvin_manager.annotations import ( ANNOTATIONS_HTML_TEMPLATE, Annotation, Annotations, BookNotes, BookmarkNotes) # Assemble the preview soup soup = BeautifulSoup(ANNOTATIONS_HTML_TEMPLATE) # Load the CSS from MXD resources path = os.path.join(self.parent.opts.resources_path, 'css', 'annotations.css') with open(path, 'rb') as f: css = f.read().decode('utf-8') style_tag = Tag(soup, 'style') style_tag.insert(0, css) soup.head.style.replaceWith(style_tag) # Assemble the sample Book notes book_notes_soup = BookNotes().construct(self.sample_book_notes) soup.body.append(book_notes_soup) cd_tag = Tag(soup, 'div', [('class', "divider")]) soup.body.append(cd_tag) # Assemble the sample Bookmark notes bookmark_notes_soup = BookmarkNotes().construct(self.sample_bookmark_notes) soup.body.append(bookmark_notes_soup) cd_tag = Tag(soup, 'div', [('class', "divider")]) soup.body.append(cd_tag) # Assemble the sample annotations pas = Annotations(None, title="Preview") pas.annotations.append(Annotation(self.sample_ann_1)) pas.annotations.append(Annotation(self.sample_ann_2)) pas.annotations.append(Annotation(self.sample_ann_3)) annotations_soup = pas.to_HTML(pas.create_soup()) soup.body.append(annotations_soup) self.parent.wv.setHtml(unicode(soup.renderContents())) def resize_row_height(self, lines, row): point_size = self.FONT.pointSize() if isosx: height = 30 + (len(lines) - 1) * (point_size + 4) elif iswindows: height = 26 + (len(lines) - 1) * (point_size + 3) elif islinux: height = 30 + (len(lines) - 1) * (point_size + 6) self.verticalHeader().resizeSection(row, height) def select_and_scroll_to_row(self, row): self.setFocus() self.selectRow(row) self.scrollToItem(self.currentItem()) def set_element_name_in_row(self, row, col, name): rule_name = QLabel(" %s " % name) rule_name.setFont(self.FONT) self.setCellWidget(row, col, rule_name) def set_css_in_row(self, row, col, css): # Clean up multi-line css formatting # A single line is 30px tall, subsequent lines add 16px lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) css_content = QPlainTextEdit('\n'.join(lines)) css_content.setFont(self.FONT) css_content.textChanged.connect(partial(self.css_edited, row)) self.setCellWidget(row, col, css_content) self.resize_row_height(lines, row) def undo_reset_button_clicked(self, mode): """ Figure out which element is being reset Reset to last save or default """ row = self.currentRow() data = self.convert_row_to_data(row) # Get default default_css = None for de in default_elements: if de['name'] == data['name']: default_css = de break # Get last saved last_saved_css = None saved_elements = self.prefs.get('appearance_css', None) last_saved_css = default_css if saved_elements: for se in saved_elements: if se['name'] == data['name']: last_saved_css = se break # Restore css if mode == 'reset': self.populate_table_row(row, default_css) elif mode == 'undo': self.populate_table_row(row, last_saved_css) # Refresh the stored data #self.prefs.set('appearance_css', self.get_data()) self.css_edited(row)
class AnnotationElementsTable(QTableWidget): ''' QTableWidget managing CSS rules ''' DEBUG = True #MAXIMUM_TABLE_HEIGHT = 113 ELEMENT_FIELD_WIDTH = 250 if isosx: FONT = QFont('Monaco', 11) elif iswindows: FONT = QFont('Lucida Console', 9) elif islinux: FONT = QFont('Monospace', 9) FONT.setStyleHint(QFont.TypeWriter) COLUMNS = { 'ELEMENT': {'ordinal': 0, 'name': 'Element'}, 'CSS': {'ordinal': 1, 'name': 'CSS'}, } sample_ann_1 = { 'text': ['Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean placerat condimentum semper. Aliquam hendrerit nisl mauris, nec laoreet orci. Donec rutrum consequat ultricies.', 'Curabitur sollicitudin euismod felis, vitae mollis magna vestibulum id.'], 'note': ['This is a note appended to the highlight.', 'And additional comments after a linebreak.'], 'highlightcolor': 'Yellow', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 4', 'location_sort': 0 } sample_ann_2 = { 'text': ['Phasellus sit amet ipsum id velit commodo convallis. In dictum felis non tellus volutpat in tincidunt neque varius. Sed at mauris augue. Vestibulum ligula nunc, ullamcorper id suscipit sed, auctor quis erat. In hac habitasse platea dictumst. Aliquam sit amet nulla dolor, ut tempus libero. In hac habitasse platea dictumst. Etiam consectetur orci vel massa eleifend in vestibulum odio auctor. Praesent orci turpis, aliquet non eleifend sit amet, sollicitudin sit amet augue.'], 'highlightcolor': 'Green', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 12', 'location_sort': 1 } sample_ann_3 = { 'text': ['Morbi massa tellus, laoreet id pretium sed, volutpat in felis.', 'Donec massa nulla, malesuada vitae volutpat quis, accumsan ut tellus.'], 'note': ['This is a note appended to the highlight.', 'And additional comments after a linebreak.'], 'highlightcolor': 'Purple', 'timestamp': time.mktime(time.localtime()), 'location': 'Chapter 53', 'location_sort': 2 } def __init__(self, parent, object_name): self.parent = parent self.prefs = parent.prefs self.elements = self.prefs.get('appearance_css', None) if not self.elements: self.elements = default_elements QTableWidget.__init__(self) self.setObjectName(object_name) self.layout = parent.elements_hl.layout() # Add ourselves to the layout sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) #sizePolicy.setVerticalStretch(0) #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) #self.setMaximumSize(QSize(16777215, self.MAXIMUM_TABLE_HEIGHT)) self.setColumnCount(0) self.setRowCount(0) self.layout.addWidget(self) def _init_controls(self): # Add the control set vbl = QVBoxLayout() self.move_element_up_tb = QToolButton() self.move_element_up_tb.setObjectName("move_element_up_tb") self.move_element_up_tb.setToolTip('Move element up') self.move_element_up_tb.setIcon(QIcon(I('arrow-up.png'))) self.move_element_up_tb.clicked.connect(self.move_row_up) vbl.addWidget(self.move_element_up_tb) self.undo_css_tb = QToolButton() self.undo_css_tb.setObjectName("undo_css_tb") self.undo_css_tb.setToolTip('Restore CSS to last saved') self.undo_css_tb.setIcon(QIcon(I('edit-undo.png'))) self.undo_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'undo')) vbl.addWidget(self.undo_css_tb) self.reset_css_tb = QToolButton() self.reset_css_tb.setObjectName("reset_css_tb") self.reset_css_tb.setToolTip('Reset CSS to default') self.reset_css_tb.setIcon(QIcon(I('trash.png'))) self.reset_css_tb.clicked.connect(partial(self.undo_reset_button_clicked, 'reset')) vbl.addWidget(self.reset_css_tb) self.move_element_down_tb = QToolButton() self.move_element_down_tb.setObjectName("move_element_down_tb") self.move_element_down_tb.setToolTip('Move element down') self.move_element_down_tb.setIcon(QIcon(I('arrow-down.png'))) self.move_element_down_tb.clicked.connect(self.move_row_down) vbl.addWidget(self.move_element_down_tb) self.layout.addLayout(vbl) def _init_table_widget(self): header_labels = [self.COLUMNS[index]['name'] \ for index in sorted(self.COLUMNS.keys(), key=lambda c: self.COLUMNS[c]['ordinal'])] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.verticalHeader().setVisible(False) self.setSortingEnabled(False) # Select single rows self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) def convert_row_to_data(self, row): data = {} data['ordinal'] = row data['name'] = unicode(self.cellWidget(row, self.COLUMNS['ELEMENT']['ordinal']).text()).strip() data['css'] = unicode(self.cellWidget(row, self.COLUMNS['CSS']['ordinal']).toPlainText()).strip() return data def css_edited(self, row): self.select_and_scroll_to_row(row) col = self.COLUMNS['CSS']['ordinal'] widget = self.cellWidget(row, col) css = unicode(widget.toPlainText()) lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) self.resize_row_height(lines, row) self.prefs.set('appearance_css', self.get_data()) widget.setFocus() self.preview_css() def get_data(self): data_items = [] for row in range(self.rowCount()): data = self.convert_row_to_data(row) data_items.append( {'ordinal': data['ordinal'], 'name': data['name'], 'css': data['css'], }) return data_items def initialize(self): self._init_table_widget() self._init_controls() self.populate_table() self.resizeColumnsToContents() self.horizontalHeader().setStretchLastSection(True) # Update preview window, select first row self.css_edited(0) def move_row(self, source, dest): self.blockSignals(True) # Save the contents of the destination row saved_data = self.convert_row_to_data(dest) # Remove the destination row self.removeRow(dest) # Insert a new row at the original location self.insertRow(source) # Populate it with the saved data self.populate_table_row(source, saved_data) self.select_and_scroll_to_row(dest) self.blockSignals(False) self.css_edited(dest) def move_row_down(self): src_row = self.currentRow() dest_row = src_row + 1 if dest_row == self.rowCount(): return self.move_row(src_row, dest_row) def move_row_up(self): src_row = self.currentRow() dest_row = src_row - 1 if dest_row < 0: return self.move_row(src_row, dest_row) def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style elements = self.elements if elements and type(elements[0]) is list: elements = elements[0] self.setFocus() elements = sorted(elements, key=lambda k: k['ordinal']) for row, element in enumerate(elements): self.insertRow(row) self.select_and_scroll_to_row(row) self.populate_table_row(row, element) self.selectRow(0) def populate_table_row(self, row, data): self.blockSignals(True) self.set_element_name_in_row(row, self.COLUMNS['ELEMENT']['ordinal'], data['name']) self.set_css_in_row(row, self.COLUMNS['CSS']['ordinal'], data['css']) self.blockSignals(False) def preview_css(self): ''' Construct a dummy annotation for preview purposes ''' from calibre_plugins.annotations.annotations import Annotation, Annotations pas = Annotations(None, title="Preview") pas.annotations.append(Annotation(self.sample_ann_1)) pas.annotations.append(Annotation(self.sample_ann_2)) pas.annotations.append(Annotation(self.sample_ann_3)) self.parent.wv.setHtml(pas.to_HTML()) def resize_row_height(self, lines, row): point_size = self.FONT.pointSize() if isosx: height = 30 + (len(lines) - 1) * (point_size + 4) elif iswindows: height = 26 + (len(lines) - 1) * (point_size + 3) elif islinux: height = 30 + (len(lines) - 1) * (point_size + 6) self.verticalHeader().resizeSection(row, height) def select_and_scroll_to_row(self, row): self.setFocus() self.selectRow(row) self.scrollToItem(self.currentItem()) def set_element_name_in_row(self, row, col, name): rule_name = QLabel(" %s " % name) rule_name.setFont(self.FONT) self.setCellWidget(row, col, rule_name) def set_css_in_row(self, row, col, css): # Clean up multi-line css formatting # A single line is 30px tall, subsequent lines add 16px lines = [] for line in css.split('\n'): lines.append(re.sub('^\s*', '', line)) css_content = QPlainTextEdit('\n'.join(lines)) css_content.setFont(self.FONT) css_content.textChanged.connect(partial(self.css_edited, row)) self.setCellWidget(row, col, css_content) self.resize_row_height(lines, row) def undo_reset_button_clicked(self, mode): """ Figure out which element is being reset Reset to last save or default """ row = self.currentRow() data = self.convert_row_to_data(row) # Get default default_css = None for de in default_elements: if de['name'] == data['name']: default_css = de break # Get last saved last_saved_css = None saved_elements = self.prefs.get('appearance_css', None) last_saved_css = default_css if saved_elements: for se in saved_elements: if se['name'] == data['name']: last_saved_css = se break # Restore css if mode == 'reset': self.populate_table_row(row, default_css) elif mode == 'undo': self.populate_table_row(row, last_saved_css) # Refresh the stored data #self.prefs.set('appearance_css', self.get_data()) self.css_edited(row)