Example #1
0
class PluginConfig(QWidget):  # {{{

    finished = pyqtSignal()

    def __init__(self, plugin, parent):
        QWidget.__init__(self, parent)

        self.plugin = plugin

        self.l = l = QVBoxLayout()
        self.setLayout(l)
        self.c = c = QLabel(_('<b>Configure %(name)s</b><br>%(desc)s') % dict(
            name=plugin.name, desc=plugin.description))
        c.setAlignment(Qt.AlignHCenter)
        l.addWidget(c)

        self.config_widget = plugin.config_widget()
        self.l.addWidget(self.config_widget)

        self.bb = QDialogButtonBox(
                QDialogButtonBox.Save|QDialogButtonBox.Cancel,
                parent=self)
        self.bb.accepted.connect(self.finished)
        self.bb.rejected.connect(self.finished)
        self.bb.accepted.connect(self.commit)
        l.addWidget(self.bb)

        self.f = QFrame(self)
        self.f.setFrameShape(QFrame.HLine)
        l.addWidget(self.f)

    def commit(self):
        self.plugin.save_settings(self.config_widget)
Example #2
0
 def setup_select_libraries_panel(self):
     self.imported_lib_widgets = []
     self.frames = []
     l = self.slp.layout()
     for lpath in sorted(self.importer.metadata['libraries'], key=lambda x:numeric_sort_key(os.path.basename(x))):
         f = QFrame(self)
         self.frames.append(f)
         l.addWidget(f)
         f.setFrameShape(f.HLine)
         w = ImportLocation(lpath, self.slp)
         l.addWidget(w)
         self.imported_lib_widgets.append(w)
     l.addStretch()
Example #3
0
class Category(QWidget):  # {{{

    plugin_activated = pyqtSignal(object)

    def __init__(self, name, plugins, gui_name, parent=None):
        QWidget.__init__(self, parent)
        self._layout = QVBoxLayout()
        self.setLayout(self._layout)
        self.label = QLabel(gui_name)
        self.sep = QFrame(self)
        self.bf = QFont()
        self.bf.setBold(True)
        self.label.setFont(self.bf)
        self.sep.setFrameShape(QFrame.HLine)
        self._layout.addWidget(self.label)
        self._layout.addWidget(self.sep)

        self.plugins = plugins

        self.bar = QToolBar(self)
        self.bar.setStyleSheet(
                'QToolBar { border: none; background: none }')
        lh = QApplication.instance().line_height
        self.bar.setIconSize(QSize(2*lh, 2*lh))
        self.bar.setMovable(False)
        self.bar.setFloatable(False)
        self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self._layout.addWidget(self.bar)
        self.actions = []
        for p in plugins:
            target = partial(self.triggered, p)
            ac = self.bar.addAction(QIcon(p.icon), p.gui_name.replace('&', '&&'), target)
            ac.setToolTip(textwrap.fill(p.description))
            ac.setWhatsThis(textwrap.fill(p.description))
            ac.setStatusTip(p.description)
            self.actions.append(ac)
            w = self.bar.widgetForAction(ac)
            w.setCursor(Qt.PointingHandCursor)
            if hasattr(w, 'setAutoRaise'):
                w.setAutoRaise(True)
            w.setMinimumWidth(100)

    def triggered(self, plugin, *args):
        self.plugin_activated.emit(plugin)
def get_frame_separator(vertical=False):
    """
    Return a frame separator

    :param vertical: define if separator is vertical or horizontal
    :type vertical: bool
    :return: frame separator
    :rtype: QFrame
    """

    line = QFrame()
    if vertical:
        line.setObjectName('vseparator')
        line.setFrameShape(QFrame.VLine)
    else:
        line.setObjectName('hseparator')
        line.setFrameShape(QFrame.HLine)

    return line
Example #5
0
class Category(QWidget):  # {{{

    plugin_activated = pyqtSignal(object)

    def __init__(self, name, plugins, gui_name, parent=None):
        QWidget.__init__(self, parent)
        self._layout = QVBoxLayout()
        self.setLayout(self._layout)
        self.label = QLabel(gui_name)
        self.sep = QFrame(self)
        self.bf = QFont()
        self.bf.setBold(True)
        self.label.setFont(self.bf)
        self.sep.setFrameShape(QFrame.HLine)
        self._layout.addWidget(self.label)
        self._layout.addWidget(self.sep)

        self.plugins = plugins

        self.bar = QToolBar(self)
        self.bar.setStyleSheet('QToolBar { border: none; background: none }')
        self.bar.setIconSize(QSize(32, 32))
        self.bar.setMovable(False)
        self.bar.setFloatable(False)
        self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self._layout.addWidget(self.bar)
        self.actions = []
        for p in plugins:
            target = partial(self.triggered, p)
            ac = self.bar.addAction(QIcon(p.icon), p.gui_name, target)
            ac.setToolTip(textwrap.fill(p.description))
            ac.setWhatsThis(textwrap.fill(p.description))
            ac.setStatusTip(p.description)
            self.actions.append(ac)
            w = self.bar.widgetForAction(ac)
            w.setCursor(Qt.PointingHandCursor)
            w.setAutoRaise(True)
            w.setMinimumWidth(100)

    def triggered(self, plugin, *args):
        self.plugin_activated.emit(plugin)
Example #6
0
class PluginConfig(QWidget):  # {{{

    finished = pyqtSignal()

    def __init__(self, plugin, parent):
        QWidget.__init__(self, parent)

        self.plugin = plugin

        self.l = l = QVBoxLayout()
        self.setLayout(l)
        self.c = c = QLabel(
            _('<b>Configure %(name)s</b><br>%(desc)s') %
            dict(name=plugin.name, desc=plugin.description))
        c.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        l.addWidget(c)

        self.config_widget = plugin.config_widget()
        self.sa = sa = QScrollArea(self)
        sa.setWidgetResizable(True)
        sa.setWidget(self.config_widget)
        l.addWidget(sa)

        self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save
                                   | QDialogButtonBox.StandardButton.Cancel,
                                   parent=self)
        self.bb.accepted.connect(self.finished)
        self.bb.rejected.connect(self.finished)
        self.bb.accepted.connect(self.commit)
        l.addWidget(self.bb)

        self.f = QFrame(self)
        self.f.setFrameShape(QFrame.Shape.HLine)
        l.addWidget(self.f)

    def commit(self):
        self.plugin.save_settings(self.config_widget)
Example #7
0
 def create_template_widget(title, which, button):
     attr = which + '_template'
     heading = QLabel('<h2>' + title)
     setattr(tp, attr + '_heading', heading)
     l.addWidget(heading)
     la = QLabel()
     setattr(self, attr, la)
     l.addWidget(la), la.setTextFormat(Qt.PlainText), la.setStyleSheet('QLabel {font-family: monospace}')
     la.setWordWrap(True)
     b = QPushButton(button)
     b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
     b.clicked.connect(partial(self.change_template, which))
     setattr(self, attr + '_button', b)
     l.addWidget(b)
     if which != 'footer':
         f = QFrame(tp)
         setattr(tp, attr + '_sep', f), f.setFrameShape(QFrame.HLine)
         l.addWidget(f)
     l.addSpacing(10)
Example #8
0
 def create_template_widget(title, which, button):
     attr = which + '_template'
     heading = QLabel('<h2>' + title)
     setattr(tp, attr + '_heading', heading)
     l.addWidget(heading)
     la = QLabel()
     setattr(self, attr, la)
     l.addWidget(la), la.setTextFormat(Qt.TextFormat.PlainText), la.setStyleSheet('QLabel {font-family: monospace}')
     la.setWordWrap(True)
     b = QPushButton(button)
     b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
     connect_lambda(b.clicked, self, lambda self: self.change_template(which))
     setattr(self, attr + '_button', b)
     l.addWidget(b)
     if which != 'footer':
         f = QFrame(tp)
         setattr(tp, attr + '_sep', f), f.setFrameShape(QFrame.Shape.HLine)
         l.addWidget(f)
     l.addSpacing(10)
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()
Example #10
0
class RuleEditor(QDialog):  # {{{

    @property
    def doing_multiple(self):
        return hasattr(self, 'multiple_icon_cb') and self.multiple_icon_cb.isChecked()

    def __init__(self, fm, pref_name, parent=None):
        QDialog.__init__(self, parent)
        self.fm = fm

        if pref_name == 'column_color_rules':
            self.rule_kind = 'color'
            rule_text = _('column coloring')
        elif pref_name == 'column_icon_rules':
            self.rule_kind = 'icon'
            rule_text = _('column icon')
        elif pref_name == 'cover_grid_icon_rules':
            self.rule_kind = 'emblem'
            rule_text = _('Cover grid emblem')

        self.setWindowIcon(QIcon(I('format-fill-color.png')))
        self.setWindowTitle(_('Create/edit a {0} rule').format(rule_text))

        self.l = l = QGridLayout(self)
        self.setLayout(l)

        self.l1 = l1 = QLabel(_('Create a {0} rule by'
            ' filling in the boxes below').format(rule_text))
        l.addWidget(l1, 0, 0, 1, 8)

        self.f1 = QFrame(self)
        self.f1.setFrameShape(QFrame.HLine)
        l.addWidget(self.f1, 1, 0, 1, 8)

        self.l2 = l2 = QLabel(_('Add the emblem:') if self.rule_kind == 'emblem' else _('Set the'))
        l.addWidget(l2, 2, 0)

        if self.rule_kind == 'color':
            l.addWidget(QLabel(_('color')))
        elif self.rule_kind == 'icon':
            self.kind_box = QComboBox(self)
            for tt, t in icon_rule_kinds:
                self.kind_box.addItem(tt, t)
            l.addWidget(self.kind_box, 2, 1)
            self.kind_box.setToolTip(textwrap.fill(_(
                'If you choose composed icons and multiple rules match, then all the'
                ' matching icons will be combined, otherwise the icon from the'
                ' first rule to match will be used.')))
        else:
            pass

        self.l3 = l3 = QLabel(_('of the column:'))
        l.addWidget(l3, 2, 2)

        self.column_box = QComboBox(self)
        l.addWidget(self.column_box, 2, 3)

        self.l4 = l4 = QLabel(_('to'))
        l.addWidget(l4, 2, 4)
        if self.rule_kind == 'emblem':
            l3.setVisible(False), self.column_box.setVisible(False), l4.setVisible(False)

        def create_filename_box():
            self.filename_box = f = QComboBox()
            self.filenamebox_view = v = QListView()
            v.setIconSize(QSize(32, 32))
            self.filename_box.setView(v)
            self.orig_filenamebox_view = f.view()
            f.setMinimumContentsLength(20), f.setSizeAdjustPolicy(f.AdjustToMinimumContentsLengthWithIcon)
            self.populate_icon_filenames()

        if self.rule_kind == 'color':
            self.color_box = ColorButton(parent=self)
            self.color_label = QLabel('Sample text Sample text')
            self.color_label.setTextFormat(Qt.RichText)
            l.addWidget(self.color_box, 2, 5)
            l.addWidget(self.color_label, 2, 6)
            l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 7)
        elif self.rule_kind == 'emblem':
            create_filename_box()
            self.update_filename_box()
            self.filename_button = QPushButton(QIcon(I('document_open.png')),
                                               _('&Add new image'))
            l.addWidget(self.filename_box)
            l.addWidget(self.filename_button, 2, 6)
            l.addWidget(QLabel(_('(Images should be square-ish)')), 2, 7)
            l.setColumnStretch(7, 10)
        else:
            create_filename_box()

            vb = QVBoxLayout()
            self.multiple_icon_cb = QCheckBox(_('Choose &more than one icon'))
            vb.addWidget(self.multiple_icon_cb)
            self.update_filename_box()
            self.multiple_icon_cb.clicked.connect(self.multiple_box_clicked)
            vb.addWidget(self.filename_box)
            l.addLayout(vb, 2, 5)

            self.filename_button = QPushButton(QIcon(I('document_open.png')),
                                               _('&Add icon'))
            l.addWidget(self.filename_button, 2, 6)
            l.addWidget(QLabel(_('Icons should be square or landscape')), 2, 7)
            l.setColumnStretch(7, 10)

        self.l5 = l5 = QLabel(
            _('Only if the following conditions are all satisfied:'))
        l.addWidget(l5, 3, 0, 1, 7)

        self.scroll_area = sa = QScrollArea(self)
        sa.setMinimumHeight(300)
        sa.setMinimumWidth(950)
        sa.setWidgetResizable(True)
        l.addWidget(sa, 4, 0, 1, 8)

        self.add_button = b = QPushButton(QIcon(I('plus.png')),
                _('Add &another condition'))
        l.addWidget(b, 5, 0, 1, 8)
        b.clicked.connect(self.add_blank_condition)

        self.l6 = l6 = QLabel(_('You can disable a condition by'
            ' blanking all of its boxes'))
        l.addWidget(l6, 6, 0, 1, 8)

        self.bb = bb = QDialogButtonBox(
                QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        l.addWidget(bb, 7, 0, 1, 8)
        if self.rule_kind != 'color':
            self.remove_button = b = bb.addButton(_('&Remove icon'), bb.ActionRole)
            b.setIcon(QIcon(I('minus.png')))
            b.setMenu(QMenu())
            b.setToolTip('<p>' + _('Remove a previously added icon. Note that doing so will cause rules that use it to stop working.'))
            self.update_remove_button()

        self.conditions_widget = QWidget(self)
        sa.setWidget(self.conditions_widget)
        self.conditions_widget.setLayout(QVBoxLayout())
        self.conditions_widget.layout().setAlignment(Qt.AlignTop)
        self.conditions = []

        if self.rule_kind == 'color':
            for b in (self.column_box, ):
                b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
                b.setMinimumContentsLength(15)

        for key in sorted(displayable_columns(fm),
                          key=lambda(k): sort_key(fm[k]['name']) if k != color_row_key else 0):
            if key == color_row_key and self.rule_kind != 'color':
                continue
            name = all_columns_string if key == color_row_key else fm[key]['name']
            if name:
                self.column_box.addItem(name, key)
        self.column_box.setCurrentIndex(0)

        if self.rule_kind == 'color':
            self.color_box.color = '#000'
            self.update_color_label()
            self.color_box.color_changed.connect(self.update_color_label)
        else:
            self.rule_icon_files = []
            self.filename_button.clicked.connect(self.filename_button_clicked)

        self.resize(self.sizeHint())

    def multiple_box_clicked(self):
        self.update_filename_box()
        self.update_icon_filenames_in_box()

    @property
    def icon_folder(self):
        return os.path.join(config_dir, 'cc_icons')

    def populate_icon_filenames(self):
        d = self.icon_folder
        self.icon_file_names = []
        if os.path.exists(d):
            for icon_file in os.listdir(d):
                icon_file = lower(icon_file)
                if os.path.exists(os.path.join(d, icon_file)) and icon_file.endswith('.png'):
                    self.icon_file_names.append(icon_file)
        self.icon_file_names.sort(key=sort_key)

    def update_filename_box(self):
        doing_multiple = self.doing_multiple

        model = QStandardItemModel()
        self.filename_box.setModel(model)
        self.icon_file_names.sort(key=sort_key)
        if doing_multiple:
            item = QStandardItem(_('Open to see checkboxes'))
            item.setIcon(QIcon(I('blank.png')))
        else:
            item = QStandardItem('')
        item.setFlags(Qt.ItemFlag(0))
        model.appendRow(item)

        for i,filename in enumerate(self.icon_file_names):
            item = QStandardItem(filename)
            if doing_multiple:
                item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                item.setData(Qt.Unchecked, Qt.CheckStateRole)
            else:
                item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            icon = QIcon(os.path.join(self.icon_folder, filename))
            item.setIcon(icon)
            model.appendRow(item)

    def update_color_label(self):
        pal = QApplication.palette()
        bg1 = unicode(pal.color(pal.Base).name())
        bg2 = unicode(pal.color(pal.AlternateBase).name())
        c = self.color_box.color
        self.color_label.setText('''
            <span style="color: {c}; background-color: {bg1}">&nbsp;{st}&nbsp;</span>
            <span style="color: {c}; background-color: {bg2}">&nbsp;{st}&nbsp;</span>
            '''.format(c=c, bg1=bg1, bg2=bg2, st=_('Sample text')))

    def sanitize_icon_file_name(self, icon_path):
        n = lower(sanitize_file_name_unicode(
                             os.path.splitext(
                                   os.path.basename(icon_path))[0]+'.png'))
        return n.replace("'", '_')

    def filename_button_clicked(self):
        try:
            path = choose_files(self, 'choose_category_icon',
                        _('Select Icon'), filters=[
                        (_('Images'), ['png', 'gif', 'jpg', 'jpeg'])],
                    all_files=False, select_only_single_file=True)
            if path:
                icon_path = path[0]
                icon_name = self.sanitize_icon_file_name(icon_path)
                if icon_name not in self.icon_file_names:
                    self.icon_file_names.append(icon_name)
                    try:
                        p = QIcon(icon_path).pixmap(QSize(128, 128))
                        d = self.icon_folder
                        if not os.path.exists(os.path.join(d, icon_name)):
                            if not os.path.exists(d):
                                os.makedirs(d)
                            with open(os.path.join(d, icon_name), 'wb') as f:
                                f.write(pixmap_to_data(p, format='PNG'))
                    except:
                        import traceback
                        traceback.print_exc()
                    self.update_filename_box()
                    self.update_remove_button()
                if self.doing_multiple:
                    if icon_name not in self.rule_icon_files:
                        self.rule_icon_files.append(icon_name)
                    self.update_icon_filenames_in_box()
                else:
                    self.filename_box.setCurrentIndex(self.filename_box.findText(icon_name))
                self.filename_box.adjustSize()
        except:
            import traceback
            traceback.print_exc()
        return

    def get_filenames_from_box(self):
        if self.doing_multiple:
            model = self.filename_box.model()
            fnames = []
            for i in range(1, model.rowCount()):
                item = model.item(i, 0)
                if item.checkState() == Qt.Checked:
                    fnames.append(lower(unicode(item.text())))
            fname = ' : '.join(fnames)
        else:
            fname = lower(unicode(self.filename_box.currentText()))
        return fname

    def update_icon_filenames_in_box(self):
        if self.rule_icon_files:
            if not self.doing_multiple:
                idx = self.filename_box.findText(self.rule_icon_files[0])
                if idx >= 0:
                    self.filename_box.setCurrentIndex(idx)
                else:
                    self.filename_box.setCurrentIndex(0)
            else:
                model = self.filename_box.model()
                for icon in self.rule_icon_files:
                    idx = self.filename_box.findText(icon)
                    if idx >= 0:
                        item = model.item(idx)
                        item.setCheckState(Qt.Checked)

    def update_remove_button(self):
        m = self.remove_button.menu()
        m.clear()
        for name in self.icon_file_names:
            m.addAction(QIcon(os.path.join(self.icon_folder, name)), name).triggered.connect(partial(
                self.remove_image, name))

    def remove_image(self, name):
        try:
            os.remove(os.path.join(self.icon_folder, name))
        except EnvironmentError:
            pass
        else:
            self.populate_icon_filenames()
            self.update_remove_button()
            self.update_filename_box()
            self.update_icon_filenames_in_box()

    def add_blank_condition(self):
        c = ConditionEditor(self.fm, parent=self.conditions_widget)
        self.conditions.append(c)
        self.conditions_widget.layout().addWidget(c)

    def apply_rule(self, kind, col, rule):
        if kind == 'color':
            if rule.color:
                self.color_box.color = rule.color
        else:
            if self.rule_kind == 'icon':
                for i, tup in enumerate(icon_rule_kinds):
                    if kind == tup[1]:
                        self.kind_box.setCurrentIndex(i)
                        break
            self.rule_icon_files = [ic.strip() for ic in rule.color.split(':')]
            if len(self.rule_icon_files) > 1:
                self.multiple_icon_cb.setChecked(True)
            self.update_filename_box()
            self.update_icon_filenames_in_box()

        for i in range(self.column_box.count()):
            c = unicode(self.column_box.itemData(i) or '')
            if col == c:
                self.column_box.setCurrentIndex(i)
                break

        for c in rule.conditions:
            ce = ConditionEditor(self.fm, parent=self.conditions_widget)
            self.conditions.append(ce)
            self.conditions_widget.layout().addWidget(ce)
            try:
                ce.condition = c
            except:
                import traceback
                traceback.print_exc()

    def accept(self):
        if self.rule_kind != 'color':
            fname = self.get_filenames_from_box()
            if not fname:
                error_dialog(self, _('No icon selected'),
                        _('You must choose an icon for this rule'), show=True)
                return
        if self.validate():
            QDialog.accept(self)

    def validate(self):
        r = Rule(self.fm)
        for c in self.conditions:
            condition = c.condition
            if condition is not None:
                try:
                    r.add_condition(*condition)
                except Exception as e:
                    import traceback
                    error_dialog(self, _('Invalid condition'),
                            _('One of the conditions for this rule is'
                                ' invalid: <b>%s</b>')%e,
                            det_msg=traceback.format_exc(), show=True)
                    return False
        if len(r.conditions) < 1:
            error_dialog(self, _('No conditions'),
                    _('You must specify at least one non-empty condition'
                        ' for this rule'), show=True)
            return False
        return True

    @property
    def rule(self):
        r = Rule(self.fm)
        if self.rule_kind != 'color':
            r.color = self.get_filenames_from_box()
        else:
            r.color = self.color_box.color
        idx = self.column_box.currentIndex()
        col = unicode(self.column_box.itemData(idx) or '')
        for c in self.conditions:
            condition = c.condition
            if condition is not None:
                r.add_condition(*condition)
        if self.rule_kind == 'icon':
            kind = unicode(self.kind_box.itemData(
                                    self.kind_box.currentIndex()) or '')
        else:
            kind = self.rule_kind

        return kind, col, r
Example #11
0
class UnpackBook(QDialog):

    def __init__(self, parent, book_id, fmts, db):
        QDialog.__init__(self, parent)
        self.setWindowIcon(QIcon(I('unpack-book.png')))
        self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db)
        self._exploded = None
        self._cleanup_dirs = []
        self._cleanup_files = []

        self.setup_ui()
        self.setWindowTitle(_('Unpack Book') + ' - ' + db.title(book_id,
            index_is_id=True))

        button = self.fmt_choice_buttons[0]
        button_map = {unicode(x.text()):x for x in self.fmt_choice_buttons}
        of = prefs['output_format'].upper()
        df = tweaks.get('default_tweak_format', None)
        lf = gprefs.get('last_tweak_format', None)
        if df and df.lower() == 'remember' and lf in button_map:
            button = button_map[lf]
        elif df and df.upper() in button_map:
            button = button_map[df.upper()]
        elif of in button_map:
            button = button_map[of]
        button.setChecked(True)

        self.init_state()
        for button in self.fmt_choice_buttons:
            button.toggled.connect(self.init_state)

    def init_state(self, *args):
        self._exploded = None
        self.preview_button.setEnabled(False)
        self.rebuild_button.setEnabled(False)
        self.explode_button.setEnabled(True)

    def setup_ui(self):  # {{{
        self._g = g = QHBoxLayout(self)
        self.setLayout(g)
        self._l = l = QVBoxLayout()
        g.addLayout(l)

        fmts = sorted(x.upper() for x in self.fmts)
        self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self)
        self._fl = fl = QHBoxLayout()
        self.fmt_choice_box.setLayout(self._fl)
        self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts]
        for x in self.fmt_choice_buttons:
            fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else
                    0)
        l.addWidget(self.fmt_choice_box)
        self.fmt_choice_box.setVisible(len(fmts) > 1)

        self.help_label = QLabel(_('''\
            <h2>About Unpack Book</h2>
            <p>Unpack Book allows you to fine tune the appearance of an ebook by
            making small changes to its internals. In order to use Unpack Book,
            you need to know a little bit about HTML and CSS, technologies that
            are used in ebooks. Follow the steps:</p>
            <br>
            <ol>
            <li>Click "Explode Book": This will "explode" the book into its
            individual internal components.<br></li>
            <li>Right click on any individual file and select "Open with..." to
            edit it in your favorite text editor.<br></li>
            <li>When you are done: <b>close the file browser window
            and the editor windows you used to make your tweaks</b>. Then click
            the "Rebuild Book" button, to update the book in your calibre
            library.</li>
            </ol>'''))
        self.help_label.setWordWrap(True)
        self._fr = QFrame()
        self._fr.setFrameShape(QFrame.VLine)
        g.addWidget(self._fr)
        g.addWidget(self.help_label)

        self._b = b = QGridLayout()
        left, top, right, bottom = b.getContentsMargins()
        top += top
        b.setContentsMargins(left, top, right, bottom)
        l.addLayout(b, stretch=10)

        self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode Book'))
        self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview Book'))
        self.cancel_button  = QPushButton(QIcon(I('window-close.png')), _('&Cancel'))
        self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild Book'))

        self.explode_button.setToolTip(
                _('Explode the book to edit its components'))
        self.preview_button.setToolTip(
                _('Preview the result of your changes'))
        self.cancel_button.setToolTip(
                _('Abort without saving any changes'))
        self.rebuild_button.setToolTip(
            _('Save your changes and update the book in the calibre library'))

        a = b.addWidget
        a(self.explode_button, 0, 0, 1, 1)
        a(self.preview_button, 0, 1, 1, 1)
        a(self.cancel_button,  1, 0, 1, 1)
        a(self.rebuild_button, 1, 1, 1, 1)

        for x in ('explode', 'preview', 'cancel', 'rebuild'):
            getattr(self, x+'_button').clicked.connect(getattr(self, x))

        self.msg = QLabel('dummy', self)
        self.msg.setVisible(False)
        self.msg.setStyleSheet('''
        QLabel {
            text-align: center;
            background-color: white;
            color: black;
            border-width: 1px;
            border-style: solid;
            border-radius: 20px;
            font-size: x-large;
            font-weight: bold;
        }
        ''')

        self.resize(self.sizeHint() + QSize(40, 10))
    # }}}

    def show_msg(self, msg):
        self.msg.setText(msg)
        self.msg.resize(self.size() - QSize(50, 25))
        self.msg.move((self.width() - self.msg.width())//2,
                (self.height() - self.msg.height())//2)
        self.msg.setVisible(True)

    def hide_msg(self):
        self.msg.setVisible(False)

    def explode(self):
        self.show_msg(_('Exploding, please wait...'))
        if len(self.fmt_choice_buttons) > 1:
            gprefs.set('last_tweak_format', self.current_format.upper())
        QTimer.singleShot(5, self.do_explode)

    def ask_question(self, msg):
        return question_dialog(self, _('Are you sure?'), msg)

    def do_explode(self):
        from calibre.ebooks.tweak import get_tools, Error, WorkerError
        tdir = PersistentTemporaryDirectory('_tweak_explode')
        self._cleanup_dirs.append(tdir)
        det_msg = None
        try:
            src = self.db.format(self.book_id, self.current_format,
                    index_is_id=True, as_path=True)
            self._cleanup_files.append(src)
            exploder = get_tools(self.current_format)[0]
            opf = exploder(src, tdir, question=self.ask_question)
        except WorkerError as e:
            det_msg = e.orig_tb
        except Error as e:
            return error_dialog(self, _('Failed to unpack'),
                (_('Could not explode the %s file.')%self.current_format) + ' '
                + as_unicode(e), show=True)
        except:
            import traceback
            det_msg = traceback.format_exc()
        finally:
            self.hide_msg()

        if det_msg is not None:
            return error_dialog(self, _('Failed to unpack'),
                _('Could not explode the %s file. Click "Show Details" for '
                    'more information.')%self.current_format, det_msg=det_msg,
                show=True)

        if opf is None:
            # The question was answered with No
            return

        self._exploded = tdir
        self.explode_button.setEnabled(False)
        self.preview_button.setEnabled(True)
        self.rebuild_button.setEnabled(True)
        open_local_file(tdir)

    def rebuild_it(self):
        from calibre.ebooks.tweak import get_tools, WorkerError
        src_dir = self._exploded
        det_msg = None
        of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower())
        of.close()
        of = of.name
        self._cleanup_files.append(of)
        try:
            rebuilder = get_tools(self.current_format)[1]
            rebuilder(src_dir, of)
        except WorkerError as e:
            det_msg = e.orig_tb
        except:
            import traceback
            det_msg = traceback.format_exc()
        finally:
            self.hide_msg()

        if det_msg is not None:
            error_dialog(self, _('Failed to rebuild file'),
                    _('Failed to rebuild %s. For more information, click '
                        '"Show details".')%self.current_format,
                    det_msg=det_msg, show=True)
            return None

        return of

    def preview(self):
        self.show_msg(_('Rebuilding, please wait...'))
        QTimer.singleShot(5, self.do_preview)

    def do_preview(self):
        rebuilt = self.rebuild_it()
        if rebuilt is not None:
            self.parent().iactions['View']._view_file(rebuilt)

    def rebuild(self):
        self.show_msg(_('Rebuilding, please wait...'))
        QTimer.singleShot(5, self.do_rebuild)

    def do_rebuild(self):
        rebuilt = self.rebuild_it()
        if rebuilt is not None:
            fmt = os.path.splitext(rebuilt)[1][1:].upper()
            with open(rebuilt, 'rb') as f:
                self.db.add_format(self.book_id, fmt, f, index_is_id=True)
            self.accept()

    def cancel(self):
        self.reject()

    def cleanup(self):
        if isosx and self._exploded:
            try:
                import appscript
                self.finder = appscript.app('Finder')
                self.finder.Finder_windows[os.path.basename(self._exploded)].close()
            except:
                pass

        for f in self._cleanup_files:
            try:
                os.remove(f)
            except:
                pass

        for d in self._cleanup_dirs:
            try:
                shutil.rmtree(d)
            except:
                pass

    @property
    def db(self):
        return self.db_ref()

    @property
    def current_format(self):
        for b in self.fmt_choice_buttons:
            if b.isChecked():
                return unicode(b.text())
Example #12
0
 def add_hline():
     f = QFrame()
     fp.f.append(f)
     f.setFrameShape(f.HLine)
     l.addRow(f)
Example #13
0
class UnpackBook(QDialog):
    def __init__(self, parent, book_id, fmts, db):
        QDialog.__init__(self, parent)
        self.setWindowIcon(QIcon(I('unpack-book.png')))
        self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db)
        self._exploded = None
        self._cleanup_dirs = []
        self._cleanup_files = []

        self.setup_ui()
        self.setWindowTitle(
            _('Unpack book') + ' - ' + db.title(book_id, index_is_id=True))

        button = self.fmt_choice_buttons[0]
        button_map = {
            unicode_type(x.text()): x
            for x in self.fmt_choice_buttons
        }
        of = prefs['output_format'].upper()
        df = tweaks.get('default_tweak_format', None)
        lf = gprefs.get('last_tweak_format', None)
        if df and df.lower() == 'remember' and lf in button_map:
            button = button_map[lf]
        elif df and df.upper() in button_map:
            button = button_map[df.upper()]
        elif of in button_map:
            button = button_map[of]
        button.setChecked(True)

        self.init_state()
        for button in self.fmt_choice_buttons:
            button.toggled.connect(self.init_state)

    def init_state(self, *args):
        self._exploded = None
        self.preview_button.setEnabled(False)
        self.rebuild_button.setEnabled(False)
        self.explode_button.setEnabled(True)

    def setup_ui(self):  # {{{
        self._g = g = QHBoxLayout(self)
        self.setLayout(g)
        self._l = l = QVBoxLayout()
        g.addLayout(l)

        fmts = sorted(x.upper() for x in self.fmts)
        self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'),
                                        self)
        self._fl = fl = QHBoxLayout()
        self.fmt_choice_box.setLayout(self._fl)
        self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts]
        for x in self.fmt_choice_buttons:
            fl.addWidget(x,
                         stretch=10 if x is self.fmt_choice_buttons[-1] else 0)
        l.addWidget(self.fmt_choice_box)
        self.fmt_choice_box.setVisible(len(fmts) > 1)

        self.help_label = QLabel(
            _('''\
            <h2>About Unpack book</h2>
            <p>Unpack book allows you to fine tune the appearance of an e-book by
            making small changes to its internals. In order to use Unpack book,
            you need to know a little bit about HTML and CSS, technologies that
            are used in e-books. Follow the steps:</p>
            <br>
            <ol>
            <li>Click "Explode book": This will "explode" the book into its
            individual internal components.<br></li>
            <li>Right click on any individual file and select "Open with..." to
            edit it in your favorite text editor.<br></li>
            <li>When you are done: <b>close the file browser window
            and the editor windows you used to make your tweaks</b>. Then click
            the "Rebuild book" button, to update the book in your calibre
            library.</li>
            </ol>'''))
        self.help_label.setWordWrap(True)
        self._fr = QFrame()
        self._fr.setFrameShape(QFrame.VLine)
        g.addWidget(self._fr)
        g.addWidget(self.help_label)

        self._b = b = QGridLayout()
        left, top, right, bottom = b.getContentsMargins()
        top += top
        b.setContentsMargins(left, top, right, bottom)
        l.addLayout(b, stretch=10)

        self.explode_button = QPushButton(QIcon(I('wizard.png')),
                                          _('&Explode book'))
        self.preview_button = QPushButton(QIcon(I('view.png')),
                                          _('&Preview book'))
        self.cancel_button = QPushButton(QIcon(I('window-close.png')),
                                         _('&Cancel'))
        self.rebuild_button = QPushButton(QIcon(I('exec.png')),
                                          _('&Rebuild book'))

        self.explode_button.setToolTip(
            _('Explode the book to edit its components'))
        self.preview_button.setToolTip(_('Preview the result of your changes'))
        self.cancel_button.setToolTip(_('Abort without saving any changes'))
        self.rebuild_button.setToolTip(
            _('Save your changes and update the book in the calibre library'))

        a = b.addWidget
        a(self.explode_button, 0, 0, 1, 1)
        a(self.preview_button, 0, 1, 1, 1)
        a(self.cancel_button, 1, 0, 1, 1)
        a(self.rebuild_button, 1, 1, 1, 1)

        for x in ('explode', 'preview', 'cancel', 'rebuild'):
            getattr(self, x + '_button').clicked.connect(getattr(self, x))

        self.msg = QLabel('dummy', self)
        self.msg.setVisible(False)
        self.msg.setStyleSheet('''
        QLabel {
            text-align: center;
            background-color: white;
            color: black;
            border-width: 1px;
            border-style: solid;
            border-radius: 20px;
            font-size: x-large;
            font-weight: bold;
        }
        ''')

        self.resize(self.sizeHint() + QSize(40, 10))

    # }}}

    def show_msg(self, msg):
        self.msg.setText(msg)
        self.msg.resize(self.size() - QSize(50, 25))
        self.msg.move((self.width() - self.msg.width()) // 2,
                      (self.height() - self.msg.height()) // 2)
        self.msg.setVisible(True)

    def hide_msg(self):
        self.msg.setVisible(False)

    def explode(self):
        self.show_msg(_('Exploding, please wait...'))
        if len(self.fmt_choice_buttons) > 1:
            gprefs.set('last_tweak_format', self.current_format.upper())
        QTimer.singleShot(5, self.do_explode)

    def ask_question(self, msg):
        return question_dialog(self, _('Are you sure?'), msg)

    def do_explode(self):
        from calibre.ebooks.tweak import get_tools, Error, WorkerError
        tdir = PersistentTemporaryDirectory('_tweak_explode')
        self._cleanup_dirs.append(tdir)
        det_msg = None
        try:
            src = self.db.format(self.book_id,
                                 self.current_format,
                                 index_is_id=True,
                                 as_path=True)
            self._cleanup_files.append(src)
            exploder = get_tools(self.current_format)[0]
            opf = exploder(src, tdir, question=self.ask_question)
        except WorkerError as e:
            det_msg = e.orig_tb
        except Error as e:
            return error_dialog(
                self,
                _('Failed to unpack'),
                (_('Could not explode the %s file.') % self.current_format) +
                ' ' + as_unicode(e),
                show=True)
        except:
            import traceback
            det_msg = traceback.format_exc()
        finally:
            self.hide_msg()

        if det_msg is not None:
            return error_dialog(
                self,
                _('Failed to unpack'),
                _('Could not explode the %s file. Click "Show Details" for '
                  'more information.') % self.current_format,
                det_msg=det_msg,
                show=True)

        if opf is None:
            # The question was answered with No
            return

        self._exploded = tdir
        self.explode_button.setEnabled(False)
        self.preview_button.setEnabled(True)
        self.rebuild_button.setEnabled(True)
        open_local_file(tdir)

    def rebuild_it(self):
        from calibre.ebooks.tweak import get_tools, WorkerError
        src_dir = self._exploded
        det_msg = None
        of = PersistentTemporaryFile('_tweak_rebuild.' +
                                     self.current_format.lower())
        of.close()
        of = of.name
        self._cleanup_files.append(of)
        try:
            rebuilder = get_tools(self.current_format)[1]
            rebuilder(src_dir, of)
        except WorkerError as e:
            det_msg = e.orig_tb
        except:
            import traceback
            det_msg = traceback.format_exc()
        finally:
            self.hide_msg()

        if det_msg is not None:
            error_dialog(self,
                         _('Failed to rebuild file'),
                         _('Failed to rebuild %s. For more information, click '
                           '"Show details".') % self.current_format,
                         det_msg=det_msg,
                         show=True)
            return None

        return of

    def preview(self):
        self.show_msg(_('Rebuilding, please wait...'))
        QTimer.singleShot(5, self.do_preview)

    def do_preview(self):
        rebuilt = self.rebuild_it()
        if rebuilt is not None:
            self.parent().iactions['View']._view_file(rebuilt)

    def rebuild(self):
        self.show_msg(_('Rebuilding, please wait...'))
        QTimer.singleShot(5, self.do_rebuild)

    def do_rebuild(self):
        rebuilt = self.rebuild_it()
        if rebuilt is not None:
            fmt = os.path.splitext(rebuilt)[1][1:].upper()
            with open(rebuilt, 'rb') as f:
                self.db.add_format(self.book_id, fmt, f, index_is_id=True)
            self.accept()

    def cancel(self):
        self.reject()

    def cleanup(self):
        if isosx and self._exploded:
            try:
                import appscript
                self.finder = appscript.app('Finder')
                self.finder.Finder_windows[os.path.basename(
                    self._exploded)].close()
            except:
                pass

        for f in self._cleanup_files:
            try:
                os.remove(f)
            except:
                pass

        for d in self._cleanup_dirs:
            try:
                shutil.rmtree(d)
            except:
                pass

    @property
    def db(self):
        return self.db_ref()

    @property
    def current_format(self):
        for b in self.fmt_choice_buttons:
            if b.isChecked():
                return unicode_type(b.text())
Example #14
0
class ComicalibreDialog(QDialog):
    """ The main dialog for users to enter options before starting. """
    def __init__(self, gui, icon, do_user_config):
        """ Initialize the gui dialog. """
        QDialog.__init__(self, gui)
        self.gui = gui
        self.icon = icon
        self.do_user_config = do_user_config
        self.db = gui.current_db
        self.create_gui()

    def start_process(self, process_type):
        """ Starts the processing worker and progress bar. """
        prefs["comic_vine_api_key"] = self.api_msg.text()
        prefs["tags_to_add"] = self.tags_msg.text()
        self.worker = ComicalibreWork(self.gui)
        self.result_text.setText("Processing...")
        self.title_start.setEnabled(False)
        self.series_start.setEnabled(False)
        self.ids_start.setEnabled(False)
        keep_tags = self.keep_tags_cb.isChecked()
        errors = self.worker.process(self.progress_bar, process_type,
                                     keep_tags)
        self.title_start.setEnabled(True)
        self.series_start.setEnabled(True)
        self.ids_start.setEnabled(True)
        results = "Finished!" + os.linesep
        for error in errors:
            results = results + error + os.linesep
        self.result_text.setText(results)

    def title_process(self):
        self.start_process(0)

    def series_process(self):
        self.start_process(1)

    def ids_process(self):
        self.start_process(2)

    def create_gui(self):
        """ Layout arrangement for the dialog and its input widgets. """
        self.layout = QVBoxLayout()
        self.layout.setSpacing(20)
        self.setLayout(self.layout)

        # Create an editable box for user to input Comic Vine API Key.
        self.api_layout = QHBoxLayout()
        self.api_msg = QLineEdit(self)
        self.api_msg.setFixedWidth(350)
        self.api_msg.setText(prefs["comic_vine_api_key"])

        self.api_label = QLabel("Comic Vine API Key")
        self.api_label.setBuddy(self.api_msg)

        self.api_layout.addWidget(self.api_label)
        self.api_layout.addWidget(self.api_msg)

        # Create an editable box for user to input Tags to add to all books.
        self.tags_layout = QHBoxLayout()
        self.tags_msg = QLineEdit(self)
        self.tags_msg.setText(prefs["tags_to_add"])

        self.tags_label = QLabel("Tags To Add")
        self.tags_label.setBuddy(self.tags_msg)

        self.tags_layout.addWidget(self.tags_label)
        self.tags_layout.addWidget(self.tags_msg)

        # Add the fields to the main layout.
        self.layout.addLayout(self.api_layout)
        self.layout.addLayout(self.tags_layout)

        # Option to keep current tags.
        self.keep_tags_cb = QCheckBox("Keep existing tags while adding new.",
                                      self)
        self.layout.addWidget(self.keep_tags_cb)

        # Separate the buttons from the input.
        self.btn_sep = QFrame()
        self.btn_sep.setFrameShape(QFrame.HLine)
        self.btn_sep.setFrameShadow(QFrame.Sunken)
        self.layout.addWidget(self.btn_sep)

        # Create a start button to kick off the processing with title.
        self.title_start = QPushButton("Using Title - Hover For Details", self)
        self.title_start.setToolTip(
            "This expects the title of a book to have a "
            "specific formatted string containing volume "
            "ID and issue#. e.g." + os.linesep + "Desired "
            "Title --- v1234 n0124" + os.linesep + "Where "
            "--- is required and the number after v "
            "matches Comic Vine's volume ID. The number "
            "after n is the issue number.")
        self.title_start.clicked.connect(self.title_process)
        self.layout.addWidget(self.title_start)

        # Create a start button to kick off the processing with series.
        self.series_start = QPushButton("Using Series - Hover For Details",
                                        self)
        self.series_start.setToolTip(
            "This expects the series name to match the "
            "Comic Vine volume ID and the series number "
            "equals issue number.")
        self.series_start.clicked.connect(self.series_process)
        self.layout.addWidget(self.series_start)

        # Create a start button to kick off the processing with CV IDs.
        self.ids_start = QPushButton("Using IDs - Hover For Details", self)
        self.ids_start.setToolTip(
            "This expects two custom columns with lookup "
            "comicvineissueid and comicvinevolumeid. "
            "These must match Comic Vine's IDs for the "
            "issue and the volume.")
        self.ids_start.clicked.connect(self.ids_process)
        self.layout.addWidget(self.ids_start)

        # Separate the progress and results.
        self.result_sep = QFrame()
        self.result_sep.setFrameShape(QFrame.HLine)
        self.result_sep.setFrameShadow(QFrame.Sunken)
        self.layout.addWidget(self.result_sep)

        # Create progress bar.
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setMinimumWidth(485)
        self.layout.addWidget(self.progress_bar)

        # Create results text area.
        self.result_box = QGroupBox()
        self.result_box.setTitle("Results")
        self.result_text = QLabel("Run Comicalibre to see results.")
        self.result_scroll = QScrollArea()
        self.result_scroll.setWidget(self.result_text)
        self.result_scroll.setWidgetResizable(True)
        self.result_layout = QVBoxLayout()
        self.result_layout.addWidget(self.result_scroll)
        self.result_box.setLayout(self.result_layout)
        self.layout.addWidget(self.result_box)

        self.setWindowTitle("Comicalibre")
        self.setWindowIcon(self.icon)
        self.resize(self.sizeHint())
Example #15
0
class MessageWidget(QWidget):
    """ Class the widget of list item to display message in chat """

    FORMAT_PATTERN = {
        r'\*\*(.+)\*\*': r'<b>\1</b>',
        '__(.+)__': r'<u>\1</u>',
        '##(.+)##': r'<i>\1</i>',
    }

    def __init__(self, user, text, time, parent=None):
        super().__init__(parent)
        self.ui()

        self.userLbl.setText(user)
        self.timeLbl.setText(time)
        self.set_text(self.apply_format(text))
        # self.msgLbl.setHtml(text)

    def ui(self):
        """ Method build ui """
        grid = QGridLayout(self)
        self.setLayout(grid)

        self.userLbl = QLabel(self)
        self.timeLbl = QLabel(self)
        self.timeLbl.setAlignment(Qt.AlignRight)

        self.line_1 = QFrame(self)
        self.line_1.setFrameShape(QFrame.HLine)
        self.line_1.setFrameShadow(QFrame.Sunken)
        self.line_2 = QFrame(self)
        self.line_2.setFrameShape(QFrame.HLine)
        self.line_2.setFrameShadow(QFrame.Sunken)

        self.msgLbl = QTextBrowser(self)
        # font = QFont()
        # font.setPointSize(12)
        # self.msgLbl.setFont(font)
        self.msgLbl.setFrameShape(QFrame.NoFrame)
        self.msgLbl.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.msgLbl.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
        self.msgLbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        grid.addWidget(self.userLbl, 0, 0)
        grid.addWidget(self.timeLbl, 0, 1)
        grid.addWidget(self.line_1, 1, 0, 1, 2)
        grid.addWidget(self.msgLbl, 2, 0, 1, 2)
        grid.addWidget(self.line_2, 3, 0, 1, 2)

    def set_text(self, text):
        size = self.msgLbl.font().pointSize() * 2
        line_count = len(text.split('<br>'))
        size = size * line_count + 6
        self.msgLbl.setMaximumHeight(size)
        self.msgLbl.setMinimumHeight(size)
        self.msgLbl.setHtml(text)

    def apply_format(self, text):
        text = text.strip()
        text = text.replace('\n', '<br>')
        for pat, fmt in self.FORMAT_PATTERN.items():
            text = re.sub(pat, fmt, text)
        return text
class ConfigWidget(QWidget, Logger):
    '''
    Config dialog for Marvin Manager
    '''

    WIZARD_PROFILES = {
        'Annotations': {
            'label': 'mm_annotations',
            'datatype': 'comments',
            'display': {},
            'is_multiple': False
        },
        'Collections': {
            'label': 'mm_collections',
            'datatype': 'text',
            'display': {u'is_names': False},
            'is_multiple': True
        },
        'Last read': {
            'label': 'mm_date_read',
            'datatype': 'datetime',
            'display': {},
            'is_multiple': False
        },
        'Locked': {
            'label': 'mm_locked',
            'datatype': 'bool',
            'display': {},
            'is_multiple': False
        },
        'Progress': {
            'label': 'mm_progress',
            'datatype': 'float',
            'display': {u'number_format': u'{0:.0f}%'},
            'is_multiple': False
        },
        'Read': {
            'label': 'mm_read',
            'datatype': 'bool',
            'display': {},
            'is_multiple': False
        },
        'Reading list': {
            'label': 'mm_reading_list',
            'datatype': 'bool',
            'display': {},
            'is_multiple': False
        },
        'Word count': {
            'label': 'mm_word_count',
            'datatype': 'int',
            'display': {u'number_format': u'{0:n}'},
            'is_multiple': False
        }
    }

    def __init__(self, plugin_action):
        QWidget.__init__(self)
        self.parent = plugin_action

        self.gui = get_gui()
        self.icon = plugin_action.icon
        self.opts = plugin_action.opts
        self.prefs = plugin_prefs
        self.resources_path = plugin_action.resources_path
        self.verbose = plugin_action.verbose

        self.restart_required = False

        self._log_location()

        self.l = QGridLayout()
        self.setLayout(self.l)
        self.column1_layout = QVBoxLayout()
        self.l.addLayout(self.column1_layout, 0, 0)
        self.column2_layout = QVBoxLayout()
        self.l.addLayout(self.column2_layout, 0, 1)

        # ----------------------------- Column 1 -----------------------------
        # ~~~~~~~~ Create the Custom fields options group box ~~~~~~~~
        self.cfg_custom_fields_gb = QGroupBox(self)
        self.cfg_custom_fields_gb.setTitle('Custom column assignments')
        self.column1_layout.addWidget(self.cfg_custom_fields_gb)

        self.cfg_custom_fields_qgl = QGridLayout(self.cfg_custom_fields_gb)
        current_row = 0

        # ++++++++ Labels + HLine ++++++++
        self.marvin_source_label = QLabel("Marvin source")
        self.cfg_custom_fields_qgl.addWidget(self.marvin_source_label, current_row, 0)
        self.calibre_destination_label = QLabel("calibre destination")
        self.cfg_custom_fields_qgl.addWidget(self.calibre_destination_label, current_row, 1)
        current_row += 1
        self.sd_hl = QFrame(self.cfg_custom_fields_gb)
        self.sd_hl.setFrameShape(QFrame.HLine)
        self.sd_hl.setFrameShadow(QFrame.Raised)
        self.cfg_custom_fields_qgl.addWidget(self.sd_hl, current_row, 0, 1, 3)
        current_row += 1

        # ++++++++ Annotations ++++++++
        self.cfg_annotations_label = QLabel('Annotations')
        self.cfg_annotations_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_annotations_label, current_row, 0)

        self.annotations_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.annotations_field_comboBox.setObjectName('annotations_field_comboBox')
        self.annotations_field_comboBox.setToolTip('Select a custom column to store Marvin annotations')
        self.cfg_custom_fields_qgl.addWidget(self.annotations_field_comboBox, current_row, 1)

        self.cfg_highlights_wizard = QToolButton()
        self.cfg_highlights_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_highlights_wizard.setToolTip("Create a custom column to store Marvin annotations")
        self.cfg_highlights_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Annotations'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_highlights_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Collections ++++++++
        self.cfg_collections_label = QLabel('Collections')
        self.cfg_collections_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_collections_label, current_row, 0)

        self.collection_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.collection_field_comboBox.setObjectName('collection_field_comboBox')
        self.collection_field_comboBox.setToolTip('Select a custom column to store Marvin collection assignments')
        self.cfg_custom_fields_qgl.addWidget(self.collection_field_comboBox, current_row, 1)

        self.cfg_collections_wizard = QToolButton()
        self.cfg_collections_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_collections_wizard.setToolTip("Create a custom column for Marvin collection assignments")
        self.cfg_collections_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Collections'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_collections_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Last read ++++++++
        self.cfg_date_read_label = QLabel("Last read")
        self.cfg_date_read_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_date_read_label, current_row, 0)

        self.date_read_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.date_read_field_comboBox.setObjectName('date_read_field_comboBox')
        self.date_read_field_comboBox.setToolTip('Select a custom column to store Last read date')
        self.cfg_custom_fields_qgl.addWidget(self.date_read_field_comboBox, current_row, 1)

        self.cfg_collections_wizard = QToolButton()
        self.cfg_collections_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_collections_wizard.setToolTip("Create a custom column to store Last read date")
        self.cfg_collections_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Last read'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_collections_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Locked ++++++++
        self.cfg_locked_label = QLabel("Locked")
        self.cfg_locked_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_locked_label, current_row, 0)

        self.locked_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.locked_field_comboBox.setObjectName('locked_field_comboBox')
        self.locked_field_comboBox.setToolTip('Select a custom column to store Locked status')
        self.cfg_custom_fields_qgl.addWidget(self.locked_field_comboBox, current_row, 1)

        self.cfg_locked_wizard = QToolButton()
        self.cfg_locked_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_locked_wizard.setToolTip("Create a custom column to store Locked status")
        self.cfg_locked_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Locked'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_locked_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Progress ++++++++
        self.cfg_progress_label = QLabel('Progress')
        self.cfg_progress_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_progress_label, current_row, 0)

        self.progress_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.progress_field_comboBox.setObjectName('progress_field_comboBox')
        self.progress_field_comboBox.setToolTip('Select a custom column to store Marvin reading progress')
        self.cfg_custom_fields_qgl.addWidget(self.progress_field_comboBox, current_row, 1)

        self.cfg_progress_wizard = QToolButton()
        self.cfg_progress_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_progress_wizard.setToolTip("Create a custom column to store Marvin reading progress")
        self.cfg_progress_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Progress'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_progress_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Read flag ++++++++
        self.cfg_read_label = QLabel('Read')
        self.cfg_read_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_read_label, current_row, 0)

        self.read_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.read_field_comboBox.setObjectName('read_field_comboBox')
        self.read_field_comboBox.setToolTip('Select a custom column to store Marvin Read status')
        self.cfg_custom_fields_qgl.addWidget(self.read_field_comboBox, current_row, 1)

        self.cfg_read_wizard = QToolButton()
        self.cfg_read_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_read_wizard.setToolTip("Create a custom column to store Marvin Read status")
        self.cfg_read_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Read'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_read_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Reading list flag ++++++++
        self.cfg_reading_list_label = QLabel('Reading list')
        self.cfg_reading_list_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_reading_list_label, current_row, 0)

        self.reading_list_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.reading_list_field_comboBox.setObjectName('reading_list_field_comboBox')
        self.reading_list_field_comboBox.setToolTip('Select a custom column to store Marvin Reading list status')
        self.cfg_custom_fields_qgl.addWidget(self.reading_list_field_comboBox, current_row, 1)

        self.cfg_reading_list_wizard = QToolButton()
        self.cfg_reading_list_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_reading_list_wizard.setToolTip("Create a custom column to store Marvin Reading list status")
        self.cfg_reading_list_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Reading list'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_reading_list_wizard, current_row, 2)
        current_row += 1

        # ++++++++ Word count ++++++++
        self.cfg_word_count_label = QLabel('Word count')
        self.cfg_word_count_label.setAlignment(Qt.AlignLeft)
        self.cfg_custom_fields_qgl.addWidget(self.cfg_word_count_label, current_row, 0)

        self.word_count_field_comboBox = QComboBox(self.cfg_custom_fields_gb)
        self.word_count_field_comboBox.setObjectName('word_count_field_comboBox')
        self.word_count_field_comboBox.setToolTip('Select a custom column to store Marvin word counts')
        self.cfg_custom_fields_qgl.addWidget(self.word_count_field_comboBox, current_row, 1)

        self.cfg_word_count_wizard = QToolButton()
        self.cfg_word_count_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_word_count_wizard.setToolTip("Create a custom column to store Marvin word counts")
        self.cfg_word_count_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Word count'))
        self.cfg_custom_fields_qgl.addWidget(self.cfg_word_count_wizard, current_row, 2)
        current_row += 1

        self.spacerItem1 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.column1_layout.addItem(self.spacerItem1)

        # ----------------------------- Column 2 -----------------------------
        # ~~~~~~~~ Create the CSS group box ~~~~~~~~
        self.cfg_css_options_gb = QGroupBox(self)
        self.cfg_css_options_gb.setTitle('CSS')
        self.column2_layout.addWidget(self.cfg_css_options_gb)
        self.cfg_css_options_qgl = QGridLayout(self.cfg_css_options_gb)

        current_row = 0

        # ++++++++ Annotations appearance ++++++++
        self.annotations_icon = QIcon(os.path.join(self.resources_path, 'icons', 'annotations_hiliter.png'))
        self.cfg_annotations_appearance_toolbutton = QToolButton()
        self.cfg_annotations_appearance_toolbutton.setIcon(self.annotations_icon)
        self.cfg_annotations_appearance_toolbutton.clicked.connect(self.configure_appearance)
        self.cfg_css_options_qgl.addWidget(self.cfg_annotations_appearance_toolbutton, current_row, 0)
        self.cfg_annotations_label = ClickableQLabel("Book notes, Bookmark notes and Annotations")
        self.cfg_annotations_label.clicked.connect(self.configure_appearance)
        self.cfg_css_options_qgl.addWidget(self.cfg_annotations_label, current_row, 1)
        current_row += 1

        # ++++++++ Injected CSS ++++++++
        self.css_editor_icon = QIcon(I('format-text-heading.png'))
        self.cfg_css_editor_toolbutton = QToolButton()
        self.cfg_css_editor_toolbutton.setIcon(self.css_editor_icon)
        self.cfg_css_editor_toolbutton.clicked.connect(self.edit_css)
        self.cfg_css_options_qgl.addWidget(self.cfg_css_editor_toolbutton, current_row, 0)
        self.cfg_css_editor_label = ClickableQLabel("Articles, Vocabulary")
        self.cfg_css_editor_label.clicked.connect(self.edit_css)
        self.cfg_css_options_qgl.addWidget(self.cfg_css_editor_label, current_row, 1)


        """
        # ~~~~~~~~ Create the Dropbox syncing group box ~~~~~~~~
        self.cfg_dropbox_syncing_gb = QGroupBox(self)
        self.cfg_dropbox_syncing_gb.setTitle('Dropbox')
        self.column2_layout.addWidget(self.cfg_dropbox_syncing_gb)
        self.cfg_dropbox_syncing_qgl = QGridLayout(self.cfg_dropbox_syncing_gb)
        current_row = 0

        # ++++++++ Syncing enabled checkbox ++++++++
        self.dropbox_syncing_checkbox = QCheckBox('Enable Dropbox updates')
        self.dropbox_syncing_checkbox.setObjectName('dropbox_syncing')
        self.dropbox_syncing_checkbox.setToolTip('Refresh custom column content from Marvin metadata')
        self.cfg_dropbox_syncing_qgl.addWidget(self.dropbox_syncing_checkbox,
            current_row, 0, 1, 3)
        current_row += 1

        # ++++++++ Dropbox folder picker ++++++++
        self.dropbox_folder_icon = QIcon(os.path.join(self.resources_path, 'icons', 'dropbox.png'))
        self.cfg_dropbox_folder_toolbutton = QToolButton()
        self.cfg_dropbox_folder_toolbutton.setIcon(self.dropbox_folder_icon)
        self.cfg_dropbox_folder_toolbutton.setToolTip("Specify Dropbox folder location on your computer")
        self.cfg_dropbox_folder_toolbutton.clicked.connect(self.select_dropbox_folder)
        self.cfg_dropbox_syncing_qgl.addWidget(self.cfg_dropbox_folder_toolbutton,
            current_row, 1)

        # ++++++++ Dropbox location lineedit ++++++++
        self.dropbox_location_lineedit = QLineEdit()
        self.dropbox_location_lineedit.setPlaceholderText("Dropbox folder location")
        self.cfg_dropbox_syncing_qgl.addWidget(self.dropbox_location_lineedit,
            current_row, 2)
        """

        # ~~~~~~~~ Create the General options group box ~~~~~~~~
        self.cfg_runtime_options_gb = QGroupBox(self)
        self.cfg_runtime_options_gb.setTitle('General options')
        self.column2_layout.addWidget(self.cfg_runtime_options_gb)
        self.cfg_runtime_options_qvl = QVBoxLayout(self.cfg_runtime_options_gb)

        # ++++++++ Temporary markers: Duplicates ++++++++
        self.duplicate_markers_checkbox = QCheckBox('Apply temporary markers to duplicate books')
        self.duplicate_markers_checkbox.setObjectName('apply_markers_to_duplicates')
        self.duplicate_markers_checkbox.setToolTip('Books with identical content will be flagged in the Library window')
        self.cfg_runtime_options_qvl.addWidget(self.duplicate_markers_checkbox)

        # ++++++++ Temporary markers: Updated ++++++++
        self.updated_markers_checkbox = QCheckBox('Apply temporary markers to books with updated content')
        self.updated_markers_checkbox.setObjectName('apply_markers_to_updated')
        self.updated_markers_checkbox.setToolTip('Books with updated content will be flagged in the Library window')
        self.cfg_runtime_options_qvl.addWidget(self.updated_markers_checkbox)

        # ++++++++ Auto refresh checkbox ++++++++
        self.auto_refresh_checkbox = QCheckBox('Automatically refresh custom column content')
        self.auto_refresh_checkbox.setObjectName('auto_refresh_at_startup')
        self.auto_refresh_checkbox.setToolTip('Update calibre custom column when Marvin XD is opened')
        self.cfg_runtime_options_qvl.addWidget(self.auto_refresh_checkbox)

        # ++++++++ Progress as percentage checkbox ++++++++
        self.reading_progress_checkbox = QCheckBox('Show reading progress as percentage')
        self.reading_progress_checkbox.setObjectName('show_progress_as_percentage')
        self.reading_progress_checkbox.setToolTip('Display percentage in Progress column')
        self.cfg_runtime_options_qvl.addWidget(self.reading_progress_checkbox)

        # ~~~~~~~~ Create the Debug options group box ~~~~~~~~
        self.cfg_debug_options_gb = QGroupBox(self)
        self.cfg_debug_options_gb.setTitle('Debug options')
        self.column2_layout.addWidget(self.cfg_debug_options_gb)
        self.cfg_debug_options_qvl = QVBoxLayout(self.cfg_debug_options_gb)

        # ++++++++ Debug logging checkboxes ++++++++
        self.debug_plugin_checkbox = QCheckBox('Enable debug logging for Marvin XD')
        self.debug_plugin_checkbox.setObjectName('debug_plugin_checkbox')
        self.debug_plugin_checkbox.setToolTip('Print plugin diagnostic messages to console')
        self.cfg_debug_options_qvl.addWidget(self.debug_plugin_checkbox)

        self.debug_libimobiledevice_checkbox = QCheckBox('Enable debug logging for libiMobileDevice')
        self.debug_libimobiledevice_checkbox.setObjectName('debug_libimobiledevice_checkbox')
        self.debug_libimobiledevice_checkbox.setToolTip('Print libiMobileDevice diagnostic messages to console')
        self.cfg_debug_options_qvl.addWidget(self.debug_libimobiledevice_checkbox)

        self.spacerItem2 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.column2_layout.addItem(self.spacerItem2)

        # ~~~~~~~~ End of construction zone ~~~~~~~~
        self.resize(self.sizeHint())

        # ~~~~~~~~ Populate/restore config options ~~~~~~~~
        #  Annotations comboBox
        self.populate_annotations()
        self.populate_collections()
        self.populate_date_read()
        self.populate_locked()
        self.populate_progress()
        self.populate_read()
        self.populate_reading_list()
        self.populate_word_count()

        """
        # Restore Dropbox settings, hook changes
        dropbox_syncing = self.prefs.get('dropbox_syncing', False)
        self.dropbox_syncing_checkbox.setChecked(dropbox_syncing)
        self.set_dropbox_syncing(dropbox_syncing)
        self.dropbox_syncing_checkbox.clicked.connect(partial(self.set_dropbox_syncing))
        self.dropbox_location_lineedit.setText(self.prefs.get('dropbox_folder', ''))
        """

        # Restore general settings
        self.duplicate_markers_checkbox.setChecked(self.prefs.get('apply_markers_to_duplicates', True))
        self.updated_markers_checkbox.setChecked(self.prefs.get('apply_markers_to_updated', True))
        self.auto_refresh_checkbox.setChecked(self.prefs.get('auto_refresh_at_startup', False))
        self.reading_progress_checkbox.setChecked(self.prefs.get('show_progress_as_percentage', False))

        # Restore debug settings, hook changes
        self.debug_plugin_checkbox.setChecked(self.prefs.get('debug_plugin', False))
        self.debug_plugin_checkbox.stateChanged.connect(self.set_restart_required)
        self.debug_libimobiledevice_checkbox.setChecked(self.prefs.get('debug_libimobiledevice', False))
        self.debug_libimobiledevice_checkbox.stateChanged.connect(self.set_restart_required)

        # Hook changes to Annotations comboBox
#         self.annotations_field_comboBox.currentIndexChanged.connect(
#             partial(self.save_combobox_setting, 'annotations_field_comboBox'))
#        self.connect(self.annotations_field_comboBox,
#                     SIGNAL('currentIndexChanged(const QString &)'),
#                     self.annotations_destination_changed)
        self.annotations_field_comboBox.currentIndexChanged.connect(self.annotations_destination_changed)
        # Launch the annotated_books_scanner
        field = get_cc_mapping('annotations', 'field', None)
        self.annotated_books_scanner = InventoryAnnotatedBooks(self.gui, field)
        self.annotated_books_scanner.signal.connect(self.inventory_complete)
        QTimer.singleShot(1, self.start_inventory)

    def annotations_destination_changed(self, qs_new_destination_name):
        '''
        If the destination field changes, move all existing annotations from old to new
        '''
        self._log_location(str(qs_new_destination_name))
        self._log("self.eligible_annotations_fields: %s" % self.eligible_annotations_fields)

        old_destination_field = get_cc_mapping('annotations', 'field', None)
        old_destination_name = get_cc_mapping('annotations', 'combobox', None)

        self._log("old_destination_field: %s" % old_destination_field)
        self._log("old_destination_name: %s" % old_destination_name)

#        new_destination_name = unicode(qs_new_destination_name)
        # Signnls available have changed. Now receivin an indec rather than a name. Can get the name
        # from the combobox
        new_destination_name = unicode(self.annotations_field_comboBox.currentText())
        self._log("new_destination_name: %s" % repr(new_destination_name))

        if old_destination_name == new_destination_name:
            self._log_location("old_destination_name = new_destination_name, no changes")
            return

        if new_destination_name == '':
            self._log_location("annotations storage disabled")
            set_cc_mapping('annotations', field=None, combobox=new_destination_name)
            return

        new_destination_field = self.eligible_annotations_fields[new_destination_name]

        if existing_annotations(self.parent, old_destination_field):
            command = self.launch_new_destination_dialog(old_destination_name, new_destination_name)

            if command == 'move':
                set_cc_mapping('annotations', field=new_destination_field, combobox=new_destination_name)

                if self.annotated_books_scanner.isRunning():
                    self.annotated_books_scanner.wait()
                move_annotations(self, self.annotated_books_scanner.annotation_map,
                    old_destination_field, new_destination_field)

            elif command == 'change':
                # Keep the updated destination field, but don't move annotations
                pass

            elif command == 'cancel':
                # Restore previous destination
                self.annotations_field_comboBox.blockSignals(True)
                old_index = self.annotations_field_comboBox.findText(old_destination_name)
                self.annotations_field_comboBox.setCurrentIndex(old_index)
                self.annotations_field_comboBox.blockSignals(False)

        else:
            # No existing annotations, just update prefs
            self._log("no existing annotations, updating destination to '{0}'".format(new_destination_name))
            set_cc_mapping('annotations', field=new_destination_field, combobox=new_destination_name)

    def configure_appearance(self):
        '''
        '''
        self._log_location()
        appearance_settings = {
                                'appearance_css': default_elements,
                                'appearance_hr_checkbox': False,
                                'appearance_timestamp_format': default_timestamp
                              }

        # Save, hash the original settings
        original_settings = {}
        osh = hashlib.md5()
        for setting in appearance_settings:
            original_settings[setting] = plugin_prefs.get(setting, appearance_settings[setting])
            osh.update(repr(plugin_prefs.get(setting, appearance_settings[setting])))

        # Display the Annotations appearance dialog
        aa = AnnotationsAppearance(self, self.annotations_icon, plugin_prefs)
        cancelled = False
        if aa.exec_():
            # appearance_hr_checkbox and appearance_timestamp_format changed live to prefs during previews
            plugin_prefs.set('appearance_css', aa.elements_table.get_data())
            # Generate a new hash
            nsh = hashlib.md5()
            for setting in appearance_settings:
                nsh.update(repr(plugin_prefs.get(setting, appearance_settings[setting])))
        else:
            for setting in appearance_settings:
                plugin_prefs.set(setting, original_settings[setting])
            nsh = osh

        # If there were changes, and there are existing annotations,
        # and there is an active Annotations field, offer to re-render
        field = get_cc_mapping('annotations', 'field', None)
        if osh.digest() != nsh.digest() and existing_annotations(self.parent, field):
            title = 'Update annotations?'
            msg = '<p>Update existing annotations to new appearance settings?</p>'
            d = MessageBox(MessageBox.QUESTION,
                           title, msg,
                           show_copy_button=False)
            self._log_location("QUESTION: %s" % msg)
            if d.exec_():
                self._log_location("Updating existing annotations to modified appearance")

                # Wait for indexing to complete
                while not self.annotated_books_scanner.isFinished():
                    Application.processEvents()

                move_annotations(self, self.annotated_books_scanner.annotation_map,
                    field, field, window_title="Updating appearance")

    def edit_css(self):
        '''
        '''
        self._log_location()
        from calibre_plugins.marvin_manager.book_status import dialog_resources_path
        klass = os.path.join(dialog_resources_path, 'css_editor.py')
        if os.path.exists(klass):
            sys.path.insert(0, dialog_resources_path)
            this_dc = importlib.import_module('css_editor')
            sys.path.remove(dialog_resources_path)
            dlg = this_dc.CSSEditorDialog(self, 'css_editor')
            dlg.initialize(self)
            dlg.exec_()

    def get_eligible_custom_fields(self, eligible_types=[], is_multiple=None):
        '''
        Discover qualifying custom fields for eligible_types[]
        '''
        #self._log_location(eligible_types)

        eligible_custom_fields = {}
        for cf in self.gui.current_db.custom_field_keys():
            cft = self.gui.current_db.metadata_for_field(cf)['datatype']
            cfn = self.gui.current_db.metadata_for_field(cf)['name']
            cfim = self.gui.current_db.metadata_for_field(cf)['is_multiple']
            #self._log("cf: %s  cft: %s  cfn: %s cfim: %s" % (cf, cft, cfn, cfim))
            if cft in eligible_types:
                if is_multiple is not None:
                    if bool(cfim) == is_multiple:
                        eligible_custom_fields[cfn] = cf
                else:
                    eligible_custom_fields[cfn] = cf
        return eligible_custom_fields

    def inventory_complete(self, msg):
        self._log_location(msg)

    def launch_cc_wizard(self, column_type):
        '''
        '''
        def _update_combo_box(comboBox, destination, previous):
            '''
            '''
            cb = getattr(self, comboBox)
            cb.blockSignals(True)
            all_items = [str(cb.itemText(i))
                         for i in range(cb.count())]
            if previous and previous in all_items:
                all_items.remove(previous)
            all_items.append(destination)

            cb.clear()
            cb.addItems(sorted(all_items, key=lambda s: s.lower()))

            # Select the new destination in the comboBox
            idx = cb.findText(destination)
            if idx > -1:
                cb.setCurrentIndex(idx)

            cb.blockSignals(False)

        from calibre_plugins.marvin_manager.book_status import dialog_resources_path

        klass = os.path.join(dialog_resources_path, 'cc_wizard.py')
        if os.path.exists(klass):
            #self._log("importing CC Wizard dialog from '%s'" % klass)
            sys.path.insert(0, dialog_resources_path)
            this_dc = importlib.import_module('cc_wizard')
            sys.path.remove(dialog_resources_path)
            dlg = this_dc.CustomColumnWizard(self,
                                             column_type,
                                             self.WIZARD_PROFILES[column_type],
                                             verbose=True)
            dlg.exec_()

            if dlg.modified_column:
                self._log("modified_column: %s" % dlg.modified_column)

                self.restart_required = True

                destination = dlg.modified_column['destination']
                label = dlg.modified_column['label']
                previous = dlg.modified_column['previous']
                source = dlg.modified_column['source']

                if source == "Annotations":
                    _update_combo_box("annotations_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_annotations_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('annotations', combobox=destination, field=label)

                elif source == 'Collections':
                    _update_combo_box("collection_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_collection_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('collections', combobox=destination, field=label)

                elif source == 'Last read':
                    _update_combo_box("date_read_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_date_read_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('date_read', combobox=destination, field=label)

                elif source == 'Locked':
                    _update_combo_box("locked_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_locked_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('locked', combobox=destination, field=label)

                elif source == "Progress":
                    _update_combo_box("progress_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_progress_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('progress', combobox=destination, field=label)

                elif source == "Read":
                    _update_combo_box("read_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_read_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('read', combobox=destination, field=label)

                elif source == "Reading list":
                    _update_combo_box("reading_list_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_reading_list_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('reading_list', combobox=destination, field=label)

                elif source == "Word count":
                    _update_combo_box("word_count_field_comboBox", destination, previous)

                    # Add/update the new destination so save_settings() can find it
                    self.eligible_word_count_fields[destination] = label

                    # Save manually in case user cancels
                    set_cc_mapping('word_count', combobox=destination, field=label)
        else:
            self._log("ERROR: Can't import from '%s'" % klass)

    def launch_new_destination_dialog(self, old, new):
        '''
        Return 'move', 'change' or 'cancel'
        '''
        from calibre_plugins.marvin_manager.book_status import dialog_resources_path
        self._log_location()
        klass = os.path.join(dialog_resources_path, 'new_destination.py')
        if os.path.exists(klass):
            self._log("importing new destination dialog from '%s'" % klass)
            sys.path.insert(0, dialog_resources_path)
            this_dc = importlib.import_module('new_destination')
            sys.path.remove(dialog_resources_path)
            dlg = this_dc.NewDestinationDialog(self, old, new)
            dlg.exec_()
            return dlg.command

    def populate_annotations(self):
        datatype = self.WIZARD_PROFILES['Annotations']['datatype']
        self.eligible_annotations_fields = self.get_eligible_custom_fields([datatype])
        self.annotations_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_annotations_fields.keys(), key=lambda s: s.lower())
        self.annotations_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('annotations', 'combobox')
        if existing:
            ci = self.annotations_field_comboBox.findText(existing)
            self.annotations_field_comboBox.setCurrentIndex(ci)

    def populate_collections(self):
        datatype = self.WIZARD_PROFILES['Collections']['datatype']
        self.eligible_collection_fields = self.get_eligible_custom_fields([datatype],
                                                                          is_multiple=True)
        self.collection_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_collection_fields.keys(), key=lambda s: s.lower())
        self.collection_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('collections', 'combobox')
        if existing:
            ci = self.collection_field_comboBox.findText(existing)
            self.collection_field_comboBox.setCurrentIndex(ci)

    def populate_date_read(self):
        #self.eligible_date_read_fields = self.get_eligible_custom_fields(['datetime'])
        datatype = self.WIZARD_PROFILES['Last read']['datatype']
        self.eligible_date_read_fields = self.get_eligible_custom_fields([datatype])
        self.date_read_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_date_read_fields.keys(), key=lambda s: s.lower())
        self.date_read_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('date_read', 'combobox')
        if existing:
            ci = self.date_read_field_comboBox.findText(existing)
            self.date_read_field_comboBox.setCurrentIndex(ci)

    def populate_locked(self):
        datatype = self.WIZARD_PROFILES['Locked']['datatype']
        self.eligible_locked_fields = self.get_eligible_custom_fields([datatype])
        self.locked_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_locked_fields.keys(), key=lambda s: s.lower())
        self.locked_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('locked', 'combobox')
        if existing:
            ci = self.locked_field_comboBox.findText(existing)
            self.locked_field_comboBox.setCurrentIndex(ci)

    def populate_progress(self):
        #self.eligible_progress_fields = self.get_eligible_custom_fields(['float'])
        datatype = self.WIZARD_PROFILES['Progress']['datatype']
        self.eligible_progress_fields = self.get_eligible_custom_fields([datatype])
        self.progress_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_progress_fields.keys(), key=lambda s: s.lower())
        self.progress_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('progress', 'combobox')
        if existing:
            ci = self.progress_field_comboBox.findText(existing)
            self.progress_field_comboBox.setCurrentIndex(ci)

    def populate_read(self):
        datatype = self.WIZARD_PROFILES['Read']['datatype']
        self.eligible_read_fields = self.get_eligible_custom_fields([datatype])
        self.read_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_read_fields.keys(), key=lambda s: s.lower())
        self.read_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('read', 'combobox')
        if existing:
            ci = self.read_field_comboBox.findText(existing)
            self.read_field_comboBox.setCurrentIndex(ci)

    def populate_reading_list(self):
        datatype = self.WIZARD_PROFILES['Reading list']['datatype']
        self.eligible_reading_list_fields = self.get_eligible_custom_fields([datatype])
        self.reading_list_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_reading_list_fields.keys(), key=lambda s: s.lower())
        self.reading_list_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('reading_list', 'combobox')
        if existing:
            ci = self.reading_list_field_comboBox.findText(existing)
            self.reading_list_field_comboBox.setCurrentIndex(ci)

    def populate_word_count(self):
        #self.eligible_word_count_fields = self.get_eligible_custom_fields(['int'])
        datatype = self.WIZARD_PROFILES['Word count']['datatype']
        self.eligible_word_count_fields = self.get_eligible_custom_fields([datatype])
        self.word_count_field_comboBox.addItems([''])
        ecf = sorted(self.eligible_word_count_fields.keys(), key=lambda s: s.lower())
        self.word_count_field_comboBox.addItems(ecf)

        # Retrieve stored value
        existing = get_cc_mapping('word_count', 'combobox')
        if existing:
            ci = self.word_count_field_comboBox.findText(existing)
            self.word_count_field_comboBox.setCurrentIndex(ci)

    """
    def select_dropbox_folder(self):
        '''
        '''
        self._log_location()
        dropbox_location = QFileDialog.getExistingDirectory(
            self,
            "Dropbox folder",
            os.path.expanduser("~"),
            QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)
        self.dropbox_location_lineedit.setText(unicode(dropbox_location))

    def set_dropbox_syncing(self, state):
        '''
        Called when checkbox changes state, or when restoring state
        Set enabled state of Dropbox folder picker to match
        '''
        self.cfg_dropbox_folder_toolbutton.setEnabled(state)
        self.dropbox_location_lineedit.setEnabled(state)
    """

    def set_restart_required(self, state):
        '''
        Set restart_required flag to show show dialog when closing dialog
        '''
        self.restart_required = True

    """
    def save_combobox_setting(self, cb, index):
        '''
        Apply changes immediately
        '''
        cf = str(getattr(self, cb).currentText())
        self._log_location("%s => %s" % (cb, repr(cf)))

        if cb == 'annotations_field_comboBox':
            field = None
            if cf:
                field = self.eligible_annotations_fields[cf]
            set_cc_mapping('annotations', combobox=cf, field=field)
    """

    def save_settings(self):
        self._log_location()

        # Annotations field
        cf = unicode(self.annotations_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_annotations_fields[cf]
        set_cc_mapping('annotations', combobox=cf, field=field)

        # Collections field
        cf = unicode(self.collection_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_collection_fields[cf]
        set_cc_mapping('collections', combobox=cf, field=field)

        # Date read field
        cf = unicode(self.date_read_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_date_read_fields[cf]
        set_cc_mapping('date_read', combobox=cf, field=field)

        # Locked field
        cf = unicode(self.locked_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_locked_fields[cf]
        set_cc_mapping('locked', combobox=cf, field=field)

        # Progress field
        cf = unicode(self.progress_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_progress_fields[cf]
        set_cc_mapping('progress', combobox=cf, field=field)

        # Read field
        cf = unicode(self.read_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_read_fields[cf]
        set_cc_mapping('read', combobox=cf, field=field)

        # Reading list field
        cf = unicode(self.reading_list_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_reading_list_fields[cf]
        set_cc_mapping('reading_list', combobox=cf, field=field)

        # Word count field
        cf = unicode(self.word_count_field_comboBox.currentText())
        field = None
        if cf:
            field = self.eligible_word_count_fields[cf]
        set_cc_mapping('word_count', combobox=cf, field=field)

        '''
        # Save Dropbox settings
        self.prefs.set('dropbox_syncing', self.dropbox_syncing_checkbox.isChecked())
        self.prefs.set('dropbox_folder', unicode(self.dropbox_location_lineedit.text()))
        '''

        # Save general settings
        self.prefs.set('apply_markers_to_duplicates', self.duplicate_markers_checkbox.isChecked())
        self.prefs.set('apply_markers_to_updated', self.updated_markers_checkbox.isChecked())
        self.prefs.set('auto_refresh_at_startup', self.auto_refresh_checkbox.isChecked())
        self.prefs.set('show_progress_as_percentage', self.reading_progress_checkbox.isChecked())

        # Save debug settings
        self.prefs.set('debug_plugin', self.debug_plugin_checkbox.isChecked())
        self.prefs.set('debug_libimobiledevice', self.debug_libimobiledevice_checkbox.isChecked())

        # If restart needed, inform user
        if self.restart_required:
            do_restart = show_restart_warning('Restart calibre for the changes to be applied.',
                                              parent=self.gui)
            if do_restart:
                self.gui.quit(restart=True)

    def start_inventory(self):
        self._log_location()
        self.annotated_books_scanner.start()
Example #17
0
    def _initUi(self):

        app = Application.instance()

        icon = QIcon(app.findResource('Subtitles.png'))

        iconLabel = QLabel()
        iconLabel.setPixmap(icon.pixmap(128))
        iconLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        appNameLabel = QLabel(self)
        appNameLabel.setText(app.applicationName())
        appNameLabel.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        appNameLabel.setStyleSheet('font-weight: bold; font-size: 18pt')
        appNameLabel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

        version_labels = self._createVersionLabels()

        licenseLabel = self._createSelectableLabel(
            'Copyright © 2018–2019 Philip Belemezov<br>'
            'Licensed under the <a href="about:blank">'
            'GNU General Public License, version 3</a>.')
        licenseLabel.setStyleSheet('color: gray; font-size: 8pt')
        licenseLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        licenseLabel.setTextFormat(Qt.RichText)
        licenseLabel.setTextInteractionFlags(Qt.TextBrowserInteraction)
        licenseLabel.linkActivated.connect(self.showLicense)

        homepageLabel = self._createLinkLabel(
            text=self.tr("Homepage"),
            href="https://github.io/philipbel/subtitles")
        homepageLabel.setAlignment(Qt.AlignCenter)

        ackLabel = self._createLinkLabel(text=self.tr("Acknowledgements"))
        ackLabel.setAlignment(Qt.AlignCenter)
        ackLabel.linkActivated.connect(self.showAcknowledgements)

        linksLayout = QVBoxLayout()
        linksLayout.addWidget(homepageLabel)
        linksLayout.addSpacing(5)
        linksLayout.addWidget(ackLabel)

        versionInfoLayout = QFormLayout()
        versionInfoLayout.setFormAlignment(Qt.AlignHCenter)
        versionInfoLayout.setHorizontalSpacing(4)
        for name, value in version_labels:
            name.setText(name.text() + ':')
            versionInfoLayout.addRow(name, value)

        mainLayout = QVBoxLayout(self)
        mainLayout.setSpacing(0)
        mainLayout.addWidget(iconLabel)
        mainLayout.addWidget(appNameLabel)
        mainLayout.addSpacing(5)
        mainLayout.addLayout(versionInfoLayout)
        mainLayout.addSpacing(20)
        mainLayout.addLayout(linksLayout)
        mainLayout.addSpacing(20)
        mainLayout.addWidget(licenseLabel)

        if sys.platform != 'darwin':
            buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
            buttonBox.setCenterButtons(True)
            buttonBox.button(QDialogButtonBox.Close).clicked.connect(
                self.reject)

            line = QFrame()
            line.setFrameShape(QFrame.HLine)
            line.setFrameShadow(QFrame.Sunken)

            mainLayout.addSpacing(8)
            mainLayout.addWidget(line)
            mainLayout.addSpacing(8)
            mainLayout.addWidget(buttonBox)
Example #18
0
class RuleEditor(QDialog):  # {{{

    @property
    def doing_multiple(self):
        return hasattr(self, 'multiple_icon_cb') and self.multiple_icon_cb.isChecked()

    def __init__(self, fm, pref_name, parent=None):
        QDialog.__init__(self, parent)
        self.fm = fm

        if pref_name == 'column_color_rules':
            self.rule_kind = 'color'
            rule_text = _('column coloring')
        elif pref_name == 'column_icon_rules':
            self.rule_kind = 'icon'
            rule_text = _('column icon')
        elif pref_name == 'cover_grid_icon_rules':
            self.rule_kind = 'emblem'
            rule_text = _('Cover grid emblem')

        self.setWindowIcon(QIcon(I('format-fill-color.png')))
        self.setWindowTitle(_('Create/edit a {0} rule').format(rule_text))

        self.l = l = QGridLayout(self)
        self.setLayout(l)

        self.l1 = l1 = QLabel(_('Create a {0} rule by'
            ' filling in the boxes below').format(rule_text))
        l.addWidget(l1, 0, 0, 1, 8)

        self.f1 = QFrame(self)
        self.f1.setFrameShape(QFrame.HLine)
        l.addWidget(self.f1, 1, 0, 1, 8)

        self.l2 = l2 = QLabel(_('Add the emblem:') if self.rule_kind == 'emblem' else _('Set the'))
        l.addWidget(l2, 2, 0)

        if self.rule_kind == 'color':
            l.addWidget(QLabel(_('color')))
        elif self.rule_kind == 'icon':
            self.kind_box = QComboBox(self)
            for tt, t in icon_rule_kinds:
                self.kind_box.addItem(tt, t)
            l.addWidget(self.kind_box, 2, 1)
            self.kind_box.setToolTip(textwrap.fill(_(
                'If you choose composed icons and multiple rules match, then all the'
                ' matching icons will be combined, otherwise the icon from the'
                ' first rule to match will be used.')))
        else:
            pass

        self.l3 = l3 = QLabel(_('of the column:'))
        l.addWidget(l3, 2, 2)

        self.column_box = QComboBox(self)
        l.addWidget(self.column_box, 2, 3)

        self.l4 = l4 = QLabel(_('to'))
        l.addWidget(l4, 2, 4)
        if self.rule_kind == 'emblem':
            l3.setVisible(False), self.column_box.setVisible(False), l4.setVisible(False)

        def create_filename_box():
            self.filename_box = f = QComboBox()
            self.filenamebox_view = v = QListView()
            v.setIconSize(QSize(32, 32))
            self.filename_box.setView(v)
            self.orig_filenamebox_view = f.view()
            f.setMinimumContentsLength(20), f.setSizeAdjustPolicy(f.AdjustToMinimumContentsLengthWithIcon)
            self.populate_icon_filenames()

        if self.rule_kind == 'color':
            self.color_box = ColorButton(parent=self)
            self.color_label = QLabel('Sample text Sample text')
            self.color_label.setTextFormat(Qt.RichText)
            l.addWidget(self.color_box, 2, 5)
            l.addWidget(self.color_label, 2, 6)
            l.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding), 2, 7)
        elif self.rule_kind == 'emblem':
            create_filename_box()
            self.update_filename_box()
            self.filename_button = QPushButton(QIcon(I('document_open.png')),
                                               _('&Add new image'))
            l.addWidget(self.filename_box)
            l.addWidget(self.filename_button, 2, 6)
            l.addWidget(QLabel(_('(Images should be square-ish)')), 2, 7)
            l.setColumnStretch(7, 10)
        else:
            create_filename_box()

            vb = QVBoxLayout()
            self.multiple_icon_cb = QCheckBox(_('Choose &more than one icon'))
            vb.addWidget(self.multiple_icon_cb)
            self.update_filename_box()
            self.multiple_icon_cb.clicked.connect(self.multiple_box_clicked)
            vb.addWidget(self.filename_box)
            l.addLayout(vb, 2, 5)

            self.filename_button = QPushButton(QIcon(I('document_open.png')),
                                               _('&Add icon'))
            l.addWidget(self.filename_button, 2, 6)
            l.addWidget(QLabel(_('Icons should be square or landscape')), 2, 7)
            l.setColumnStretch(7, 10)

        self.l5 = l5 = QLabel(
            _('Only if the following conditions are all satisfied:'))
        l.addWidget(l5, 3, 0, 1, 7)

        self.scroll_area = sa = QScrollArea(self)
        sa.setMinimumHeight(300)
        sa.setMinimumWidth(950)
        sa.setWidgetResizable(True)
        l.addWidget(sa, 4, 0, 1, 8)

        self.add_button = b = QPushButton(QIcon(I('plus.png')),
                _('Add &another condition'))
        l.addWidget(b, 5, 0, 1, 8)
        b.clicked.connect(self.add_blank_condition)

        self.l6 = l6 = QLabel(_('You can disable a condition by'
            ' blanking all of its boxes'))
        l.addWidget(l6, 6, 0, 1, 8)

        self.bb = bb = QDialogButtonBox(
                QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        l.addWidget(bb, 7, 0, 1, 8)
        if self.rule_kind != 'color':
            self.remove_button = b = bb.addButton(_('&Remove icon'), bb.ActionRole)
            b.setIcon(QIcon(I('minus.png')))
            b.setMenu(QMenu())
            b.setToolTip('<p>' + _('Remove a previously added icon. Note that doing so will cause rules that use it to stop working.'))
            self.update_remove_button()

        self.conditions_widget = QWidget(self)
        sa.setWidget(self.conditions_widget)
        self.conditions_widget.setLayout(QVBoxLayout())
        self.conditions_widget.layout().setAlignment(Qt.AlignTop)
        self.conditions = []

        if self.rule_kind == 'color':
            for b in (self.column_box, ):
                b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
                b.setMinimumContentsLength(15)

        for key in sorted(displayable_columns(fm),
                          key=lambda k: sort_key(fm[k]['name']) if k != color_row_key else 0):
            if key == color_row_key and self.rule_kind != 'color':
                continue
            name = all_columns_string if key == color_row_key else fm[key]['name']
            if name:
                self.column_box.addItem(name, key)
        self.column_box.setCurrentIndex(0)

        if self.rule_kind == 'color':
            self.color_box.color = '#000'
            self.update_color_label()
            self.color_box.color_changed.connect(self.update_color_label)
        else:
            self.rule_icon_files = []
            self.filename_button.clicked.connect(self.filename_button_clicked)

        self.resize(self.sizeHint())

    def multiple_box_clicked(self):
        self.update_filename_box()
        self.update_icon_filenames_in_box()

    @property
    def icon_folder(self):
        return os.path.join(config_dir, 'cc_icons')

    def populate_icon_filenames(self):
        d = self.icon_folder
        self.icon_file_names = []
        if os.path.exists(d):
            for icon_file in os.listdir(d):
                icon_file = lower(icon_file)
                if os.path.exists(os.path.join(d, icon_file)) and icon_file.endswith('.png'):
                    self.icon_file_names.append(icon_file)
        self.icon_file_names.sort(key=sort_key)

    def update_filename_box(self):
        doing_multiple = self.doing_multiple

        model = QStandardItemModel()
        self.filename_box.setModel(model)
        self.icon_file_names.sort(key=sort_key)
        if doing_multiple:
            item = QStandardItem(_('Open to see checkboxes'))
            item.setIcon(QIcon(I('blank.png')))
        else:
            item = QStandardItem('')
        item.setFlags(Qt.ItemFlag(0))
        model.appendRow(item)

        for i,filename in enumerate(self.icon_file_names):
            item = QStandardItem(filename)
            if doing_multiple:
                item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                item.setData(Qt.Unchecked, Qt.CheckStateRole)
            else:
                item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            icon = QIcon(os.path.join(self.icon_folder, filename))
            item.setIcon(icon)
            model.appendRow(item)

    def update_color_label(self):
        pal = QApplication.palette()
        bg1 = unicode_type(pal.color(pal.Base).name())
        bg2 = unicode_type(pal.color(pal.AlternateBase).name())
        c = self.color_box.color
        self.color_label.setText('''
            <span style="color: {c}; background-color: {bg1}">&nbsp;{st}&nbsp;</span>
            <span style="color: {c}; background-color: {bg2}">&nbsp;{st}&nbsp;</span>
            '''.format(c=c, bg1=bg1, bg2=bg2, st=_('Sample text')))

    def sanitize_icon_file_name(self, icon_path):
        n = lower(sanitize_file_name_unicode(
                             os.path.splitext(
                                   os.path.basename(icon_path))[0]+'.png'))
        return n.replace("'", '_')

    def filename_button_clicked(self):
        try:
            path = choose_files(self, 'choose_category_icon',
                        _('Select Icon'), filters=[
                        (_('Images'), ['png', 'gif', 'jpg', 'jpeg'])],
                    all_files=False, select_only_single_file=True)
            if path:
                icon_path = path[0]
                icon_name = self.sanitize_icon_file_name(icon_path)
                if icon_name not in self.icon_file_names:
                    self.icon_file_names.append(icon_name)
                    try:
                        p = QIcon(icon_path).pixmap(QSize(128, 128))
                        d = self.icon_folder
                        if not os.path.exists(os.path.join(d, icon_name)):
                            if not os.path.exists(d):
                                os.makedirs(d)
                            with open(os.path.join(d, icon_name), 'wb') as f:
                                f.write(pixmap_to_data(p, format='PNG'))
                    except:
                        import traceback
                        traceback.print_exc()
                    self.update_filename_box()
                    self.update_remove_button()
                if self.doing_multiple:
                    if icon_name not in self.rule_icon_files:
                        self.rule_icon_files.append(icon_name)
                    self.update_icon_filenames_in_box()
                else:
                    self.filename_box.setCurrentIndex(self.filename_box.findText(icon_name))
                self.filename_box.adjustSize()
        except:
            import traceback
            traceback.print_exc()
        return

    def get_filenames_from_box(self):
        if self.doing_multiple:
            model = self.filename_box.model()
            fnames = []
            for i in range(1, model.rowCount()):
                item = model.item(i, 0)
                if item.checkState() == Qt.Checked:
                    fnames.append(lower(unicode_type(item.text())))
            fname = ' : '.join(fnames)
        else:
            fname = lower(unicode_type(self.filename_box.currentText()))
        return fname

    def update_icon_filenames_in_box(self):
        if self.rule_icon_files:
            if not self.doing_multiple:
                idx = self.filename_box.findText(self.rule_icon_files[0])
                if idx >= 0:
                    self.filename_box.setCurrentIndex(idx)
                else:
                    self.filename_box.setCurrentIndex(0)
            else:
                model = self.filename_box.model()
                for icon in self.rule_icon_files:
                    idx = self.filename_box.findText(icon)
                    if idx >= 0:
                        item = model.item(idx)
                        item.setCheckState(Qt.Checked)

    def update_remove_button(self):
        m = self.remove_button.menu()
        m.clear()
        for name in self.icon_file_names:
            ac = m.addAction(QIcon(os.path.join(self.icon_folder, name)), name)
            connect_lambda(ac.triggered, self, lambda self: self.remove_image(self.sender().text()))

    def remove_image(self, name):
        try:
            os.remove(os.path.join(self.icon_folder, name))
        except EnvironmentError:
            pass
        else:
            self.populate_icon_filenames()
            self.update_remove_button()
            self.update_filename_box()
            self.update_icon_filenames_in_box()

    def add_blank_condition(self):
        c = ConditionEditor(self.fm, parent=self.conditions_widget)
        self.conditions.append(c)
        self.conditions_widget.layout().addWidget(c)

    def apply_rule(self, kind, col, rule):
        if kind == 'color':
            if rule.color:
                self.color_box.color = rule.color
        else:
            if self.rule_kind == 'icon':
                for i, tup in enumerate(icon_rule_kinds):
                    if kind == tup[1]:
                        self.kind_box.setCurrentIndex(i)
                        break
            self.rule_icon_files = [ic.strip() for ic in rule.color.split(':')]
            if len(self.rule_icon_files) > 1:
                self.multiple_icon_cb.setChecked(True)
            self.update_filename_box()
            self.update_icon_filenames_in_box()

        for i in range(self.column_box.count()):
            c = unicode_type(self.column_box.itemData(i) or '')
            if col == c:
                self.column_box.setCurrentIndex(i)
                break

        for c in rule.conditions:
            ce = ConditionEditor(self.fm, parent=self.conditions_widget)
            self.conditions.append(ce)
            self.conditions_widget.layout().addWidget(ce)
            try:
                ce.condition = c
            except:
                import traceback
                traceback.print_exc()

    def accept(self):
        if self.rule_kind != 'color':
            fname = self.get_filenames_from_box()
            if not fname:
                error_dialog(self, _('No icon selected'),
                        _('You must choose an icon for this rule'), show=True)
                return
        if self.validate():
            QDialog.accept(self)

    def validate(self):
        r = Rule(self.fm)
        for c in self.conditions:
            condition = c.condition
            if condition is not None:
                try:
                    r.add_condition(*condition)
                except Exception as e:
                    import traceback
                    error_dialog(self, _('Invalid condition'),
                            _('One of the conditions for this rule is'
                                ' invalid: <b>%s</b>')%e,
                            det_msg=traceback.format_exc(), show=True)
                    return False
        if len(r.conditions) < 1:
            error_dialog(self, _('No conditions'),
                    _('You must specify at least one non-empty condition'
                        ' for this rule'), show=True)
            return False
        return True

    @property
    def rule(self):
        r = Rule(self.fm)
        if self.rule_kind != 'color':
            r.color = self.get_filenames_from_box()
        else:
            r.color = self.color_box.color
        idx = self.column_box.currentIndex()
        col = unicode_type(self.column_box.itemData(idx) or '')
        for c in self.conditions:
            condition = c.condition
            if condition is not None:
                r.add_condition(*condition)
        if self.rule_kind == 'icon':
            kind = unicode_type(self.kind_box.itemData(
                                    self.kind_box.currentIndex()) or '')
        else:
            kind = self.rule_kind

        return kind, col, r
Example #19
0
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()
Example #20
0
    def __init__(self, text, colors, color_caption):
        TestableWidget.__init__(self)
        self.setWindowFlags(Qt.Popup)

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        border = "border: 1px solid #9b9b9b;"
        self.setStyleSheet('QFrame[frameShape="4"]{  color: #9b9b9b;}')

        # -- CAPTION ----------------------------------
        caption = QFrame(self, flags=Qt.WindowFlags())
        caption_layout = QHBoxLayout()
        self._lbl_caption = QLabel(text)
        self._lbl_caption.setStyleSheet("border: 0px solid blue;")
        caption_layout.addWidget(self._lbl_caption, alignment=Qt.Alignment())
        caption.setLayout(caption_layout)
        caption_layout.setContentsMargins(9, 5, 9, 5)
        caption.setStyleSheet(f"background-color: {color_caption}; {border}")

        # -- COLORS GRID ------------------------------
        colorbox = QFrame(self, flags=Qt.WindowFlags())
        color_layout = QGridLayout()
        colorbox.setLayout(color_layout)
        color_layout.setContentsMargins(9, 0, 9, 0)
        nn = 7
        self.clrbtn = []
        for i, color in enumerate(colors):
            self.clrbtn.append(QToolButton())
            css = (f"QToolButton {{{border}background-color:{color};}}"
                   f"QToolButton:hover {{border: 1px solid red; }}")
            self.clrbtn[-1].setStyleSheet(css)
            self.clrbtn[-1].clicked.connect(
                lambda x, c=color: self.select_color_(c))
            # noinspection PyArgumentList
            color_layout.addWidget(self.clrbtn[-1], i // nn, i % nn)

        # -- SPLITTER ---------------------------------
        h_frame = QFrame(None, flags=Qt.WindowFlags())
        h_frame.setFrameShape(QFrame.HLine)
        h_frame.setContentsMargins(0, 0, 0, 0)

        # -- BOTTOM (other color) ---------------------
        btns = QFrame(self, flags=Qt.WindowFlags())
        btn_layout = QHBoxLayout()
        other = QToolButton()
        other.clicked.connect(self.other_color_)
        other.setAutoRaise(True)
        other.setIcon(QIcon(img("editor/colorwheel")))
        btn_layout.addWidget(other, alignment=Qt.Alignment())
        self._lbl_other = QLabel(self.tr("other colors"))
        btn_layout.addWidget(self._lbl_other, alignment=Qt.Alignment())
        btns.setLayout(btn_layout)
        btn_layout.setContentsMargins(9, 0, 9, 9)
        self.clrbtn.append(other)

        # ---------------------------------------------
        main_layout.addWidget(caption, alignment=Qt.Alignment())
        main_layout.addWidget(colorbox, alignment=Qt.Alignment())
        main_layout.addWidget(h_frame, alignment=Qt.Alignment())
        main_layout.addWidget(btns, alignment=Qt.Alignment())

        self.setLayout(main_layout)
Example #21
0
    def __init__(self, color_caption):
        TestableWidget.__init__(self)
        self.setWindowFlags(Qt.Popup)
        self._text = self.tr("Table")

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)

        b = "border: 1px solid #9b9b9b;"
        self._styles = {
            "border": b,
            "border_blue": "border: 0px solid blue;",
            "selection": f"QToolButton {{{b}background-color:#fee5e2;}}",
            "white": f"QToolButton {{{b}background-color:white;}}",
            "frame": 'QFrame[frameShape="4"]{color: #9b9b9b;}'}

        self.setStyleSheet(self._styles["frame"])

        # -- CAPTION ----------------------------------
        caption = QFrame(self, flags=Qt.WindowFlags())
        caption_layout = QHBoxLayout()
        self._lbl_caption = QLabel(self._text)
        self._lbl_caption.setStyleSheet(self._styles["border_blue"])
        caption_layout.addWidget(self._lbl_caption, alignment=Qt.Alignment())
        caption.setLayout(caption_layout)
        caption_layout.setContentsMargins(9, 5, 9, 5)
        caption.setStyleSheet(
            f"background-color: {color_caption}; {self._styles['border']}")

        # -- CELLS GRID -------------------------------
        cellsbox = QFrame(self, flags=Qt.WindowFlags())
        cells = QGridLayout()
        cells.setSpacing(1)
        cellsbox.setLayout(cells)
        cells.setContentsMargins(9, 0, 9, 0)
        self._nn = 10
        self._clrbtn = []
        colors = ["white" for _ in range(self._nn ** 2)]
        cellsbox.leaveEvent = lambda x: self._leave_cell()
        for i, color in enumerate(colors):
            self._clrbtn.append(QToolButton())
            # noinspection PyPep8Naming
            self._clrbtn[-1].enterEvent = lambda x, n=i: self._enter_cell(n)
            self._clrbtn[-1].setStyleSheet(self._styles["white"])
            self._clrbtn[-1].clicked.connect(
                lambda x, n=i: self.select_size_(n))
            # noinspection PyArgumentList
            cells.addWidget(self._clrbtn[-1], i // self._nn, i % self._nn)

        # -- SPLITTER ---------------------------------
        h_frame = QFrame(None, flags=Qt.WindowFlags())
        h_frame.setFrameShape(QFrame.HLine)
        h_frame.setContentsMargins(0, 0, 0, 0)

        # -- BOTTOM (other color) ---------------------
        btns = QFrame(self, flags=Qt.WindowFlags())
        btn_layout = QHBoxLayout()
        other = QToolButton()
        other.clicked.connect(self.other_size_)
        other.setAutoRaise(True)
        other.setIcon(QIcon(img("editor/table_gray")))
        btn_layout.addWidget(other, alignment=Qt.Alignment())
        self._lbl_other = QLabel(self.tr("insert table"))
        btn_layout.addWidget(self._lbl_other, alignment=Qt.Alignment())
        btns.setLayout(btn_layout)
        btn_layout.setContentsMargins(9, 0, 9, 9)
        self._clrbtn.append(other)

        # ---------------------------------------------
        main_layout.addWidget(caption, alignment=Qt.Alignment())
        main_layout.addWidget(cellsbox, alignment=Qt.Alignment())
        main_layout.addWidget(h_frame, alignment=Qt.Alignment())
        main_layout.addWidget(btns, alignment=Qt.Alignment())

        self.setLayout(main_layout)
Example #22
0
    def __init__(self, parent, old, new):
        super(QDialog, self).__init__(parent.gui)
        self.db = parent.gui.current_db
        self.gui = parent.gui

        self._log_location()

        layout = QVBoxLayout()
        self.setLayout(layout)
        header = QLabel(_("Move annotations or change destination?"))
        header.setAlignment(Qt.AlignCenter)
        header_font = QFont()
        header_font.setPointSize(16)
        header.setFont(header_font)
        layout.addWidget(header)
        change_group = QGroupBox("", self)
        layout.addWidget(change_group)
        self.gl = QGridLayout()
        change_group.setLayout(self.gl)

        horizontal_line = QFrame(self)
        horizontal_line.setGeometry(QRect(0, 0, 1, 3))
        horizontal_line.setFrameShape(QFrame.HLine)
        horizontal_line.setFrameShadow(QFrame.Raised)
        self.gl.addWidget(horizontal_line, 1, 0, 1, 2)

        self.move_button = QPushButton(_("Move"))
        self.gl.addWidget(self.move_button, 3, 0, 1, 1)
        self.move_label = QLabel(
            _('<html><head/><body><p>&bull; Move existing annotations from <span style=" font-weight:600;">{old}</span> to <span style=" font-weight:600;">{new}</span>.<br/>&bull; Existing annotations will be removed from <span style=" font-weight:600;">{old}</span>.<br/>&bull; Newly imported annotations will be added to <span style=" font-weight:600;">{new}</span>.</p></body></html>'
              ))
        self.move_label.setSizePolicy(QSizePolicy.MinimumExpanding,
                                      QSizePolicy.MinimumExpanding)
        self.gl.addWidget(self.move_label, 3, 1)
        self.gl.addWidget(horizontal_line, 4, 1)

        self.change_button = QPushButton(_("Change"))
        self.gl.addWidget(self.change_button, 5, 0, 1, 1)
        self.change_label = QLabel(
            _('<html><head/><body><p>&bull; Change annotations storage from <span style=" font-weight:600;">{old}</span> to <span style=" font-weight:600;">{new}</span>.<br/>&bull; Existing annotations will remain in <span style=" font-weight:600;">{old}</span>.<br/>&bull; Newly imported annotations will be added to <span style=" font-weight:600;">{new}</span>.</p></body></html>'
              ))
        self.change_label.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.gl.addWidget(self.change_label, 5, 1, 1, 1)

        self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
        layout.addWidget(self.bb)
        # Hook the button events
        self.bb.clicked.connect(partial(self.button_clicked, 'cancel'))
        self.move_button.clicked.connect(partial(self.button_clicked, 'move'))
        self.change_button.clicked.connect(
            partial(self.button_clicked, 'change'))

        # Customize the dialog text
        self.move_label.setText(
            str(self.move_label.text()).format(old=old, new=new))
        self.change_label.setText(
            str(self.change_label.text()).format(old=old, new=new))

        self.command = 'cancel'
        self.do_resize()
Example #23
0
 def add_hline():
     f = QFrame()
     fp.f.append(f)
     f.setFrameShape(QFrame.Shape.HLine)
     l.addRow(f)
Example #24
0
class ConfigWidget(QWidget, Logger):
    # Manually managed controls when saving/restoring
    EXCLUDED_CONTROLS = [
        'cfg_annotations_destination_comboBox'
        ]

    #LOCATION_TEMPLATE = "{cls}:{func}({arg1}) {arg2}"

    WIZARD_PROFILES = {
        'Annotations': {
            'label': 'mm_annotations',
            'datatype': 'comments',
            'display': {},
            'is_multiple': False
            }
        }

    def __init__(self, plugin_action):
        self.gui = plugin_action.gui
        self.opts = plugin_action.opts

        QWidget.__init__(self)
        self.l = QVBoxLayout()
        self.setLayout(self.l)

        # ~~~~~~~~ Create the runtime options group box ~~~~~~~~
        self.cfg_runtime_options_gb = QGroupBox(self)
        self.cfg_runtime_options_gb.setTitle(_('Runtime options'))
        self.l.addWidget(self.cfg_runtime_options_gb)
        self.cfg_runtime_options_qvl = QVBoxLayout(self.cfg_runtime_options_gb)

        # ~~~~~~~~ Disable caching checkbox ~~~~~~~~
        self.cfg_disable_caching_checkbox = QCheckBox(_('Disable caching'))
        self.cfg_disable_caching_checkbox.setObjectName('cfg_disable_caching_checkbox')
        self.cfg_disable_caching_checkbox.setToolTip(_('Force reload of reader database'))
        self.cfg_disable_caching_checkbox.setChecked(False)
        self.cfg_runtime_options_qvl.addWidget(self.cfg_disable_caching_checkbox)

        # ~~~~~~~~ plugin logging checkbox ~~~~~~~~
        self.cfg_plugin_debug_log_checkbox = QCheckBox(_('Enable debug logging for Annotations plugin'))
        self.cfg_plugin_debug_log_checkbox.setObjectName('cfg_plugin_debug_log_checkbox')
        self.cfg_plugin_debug_log_checkbox.setToolTip(_('Print plugin diagnostic messages to console'))
        self.cfg_plugin_debug_log_checkbox.setChecked(False)
        self.cfg_runtime_options_qvl.addWidget(self.cfg_plugin_debug_log_checkbox)

        # ~~~~~~~~ libiMobileDevice logging checkbox ~~~~~~~~
        self.cfg_libimobiledevice_debug_log_checkbox = QCheckBox(_('Enable debug logging for libiMobileDevice'))
        self.cfg_libimobiledevice_debug_log_checkbox.setObjectName('cfg_libimobiledevice_debug_log_checkbox')
        self.cfg_libimobiledevice_debug_log_checkbox.setToolTip(_('Print libiMobileDevice debug messages to console'))
        self.cfg_libimobiledevice_debug_log_checkbox.setChecked(False)
        self.cfg_libimobiledevice_debug_log_checkbox.setEnabled(LIBIMOBILEDEVICE_AVAILABLE)
        self.cfg_runtime_options_qvl.addWidget(self.cfg_libimobiledevice_debug_log_checkbox)

        # ~~~~~~~~ Create the Annotations options group box ~~~~~~~~
        self.cfg_annotation_options_gb = QGroupBox(self)
        self.cfg_annotation_options_gb.setTitle(_('Annotation options'))
        self.l.addWidget(self.cfg_annotation_options_gb)

        self.cfg_annotation_options_qgl = QGridLayout(self.cfg_annotation_options_gb)
        current_row = 0

        # Add the label/combobox for annotations destination
        self.cfg_annotations_destination_label = QLabel(_('<b>Add fetched annotations to<b>'))
        self.cfg_annotations_destination_label.setAlignment(Qt.AlignLeft)
        self.cfg_annotation_options_qgl.addWidget(self.cfg_annotations_destination_label, current_row, 0)
        current_row += 1

        self.cfg_annotations_destination_comboBox = QComboBox(self.cfg_annotation_options_gb)
        self.cfg_annotations_destination_comboBox.setObjectName('cfg_annotations_destination_comboBox')
        self.cfg_annotations_destination_comboBox.setToolTip(_('Custom field to store annotations'))
        self.cfg_annotation_options_qgl.addWidget(self.cfg_annotations_destination_comboBox, current_row, 0)

        # Populate annotations_field combobox
        db = self.gui.current_db
        all_custom_fields = db.custom_field_keys()
        self.custom_fields = {}
        for custom_field in all_custom_fields:
            field_md = db.metadata_for_field(custom_field)
            if field_md['datatype'] in ['comments']:
                self.custom_fields[field_md['name']] = {'field': custom_field,
                                                   'datatype': field_md['datatype']}

        all_fields = self.custom_fields.keys() + ['Comments']
        for cf in sorted(all_fields):
            self.cfg_annotations_destination_comboBox.addItem(cf)

        # Add CC Wizard
        self.cfg_annotations_wizard = QToolButton()
        self.cfg_annotations_wizard.setIcon(QIcon(I('wizard.png')))
        self.cfg_annotations_wizard.setToolTip(_("Create a custom column to store annotations"))
        self.cfg_annotations_wizard.clicked.connect(partial(self.launch_cc_wizard, 'Annotations'))
        self.cfg_annotation_options_qgl.addWidget(self.cfg_annotations_wizard, current_row, 2)

        current_row += 1

        # ~~~~~~~~ Add a horizontal line ~~~~~~~~
        self.cfg_appearance_hl = QFrame(self)
        self.cfg_appearance_hl.setGeometry(QRect(0, 0, 1, 3))
        self.cfg_appearance_hl.setFrameShape(QFrame.HLine)
        self.cfg_appearance_hl.setFrameShadow(QFrame.Raised)
        self.cfg_annotation_options_qgl.addWidget(self.cfg_appearance_hl, current_row, 0)
        current_row += 1

        # ~~~~~~~~ Add the Modify… button ~~~~~~~~
        self.cfg_annotations_appearance_pushbutton = QPushButton(_("Modify appearance…"))
        self.cfg_annotations_appearance_pushbutton.clicked.connect(self.configure_appearance)
        self.cfg_annotation_options_qgl.addWidget(self.cfg_annotations_appearance_pushbutton, current_row, 0)
        current_row += 1

        self.spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
        self.cfg_annotation_options_qgl.addItem(self.spacerItem, current_row, 0, 1, 1)

        # ~~~~~~~~ Compilations group box ~~~~~~~~
        self.cfg_compilation_options_gb = QGroupBox(self)
        self.cfg_compilation_options_gb.setTitle(_('Compilations'))
        self.l.addWidget(self.cfg_compilation_options_gb)
        self.cfg_compilation_options_qgl = QGridLayout(self.cfg_compilation_options_gb)
        current_row = 0

        #   News clippings
        self.cfg_news_clippings_checkbox = QCheckBox(_('Collect News clippings'))
        self.cfg_news_clippings_checkbox.setObjectName('cfg_news_clippings_checkbox')
        self.cfg_compilation_options_qgl.addWidget(self.cfg_news_clippings_checkbox,
            current_row, 0)

        self.cfg_news_clippings_lineEdit = QLineEdit()
        self.cfg_news_clippings_lineEdit.setObjectName('cfg_news_clippings_lineEdit')
        self.cfg_news_clippings_lineEdit.setToolTip(_('Title for collected news clippings'))
        self.cfg_compilation_options_qgl.addWidget(self.cfg_news_clippings_lineEdit,
            current_row, 1)

        # ~~~~~~~~ End of construction zone ~~~~~~~~

        self.resize(self.sizeHint())

        # Restore state of controls, populate annotations combobox
        self.controls = inventory_controls(self, dump_controls=False)
        restore_state(self)
        self.populate_annotations()

        # Hook changes to annotations_destination_combobox
#        self.connect(self.cfg_annotations_destination_comboBox,
#                     pyqtSignal('currentIndexChanged(const QString &)'),
#                     self.annotations_destination_changed)
        self.cfg_annotations_destination_comboBox.currentIndexChanged.connect(self.annotations_destination_changed)

        # Hook changes to diagnostic checkboxes
        self.cfg_disable_caching_checkbox.stateChanged.connect(self.restart_required)
        self.cfg_libimobiledevice_debug_log_checkbox.stateChanged.connect(self.restart_required)
        self.cfg_plugin_debug_log_checkbox.stateChanged.connect(self.restart_required)

        # Hook changes to News clippings, initialize
        self.cfg_news_clippings_checkbox.stateChanged.connect(self.news_clippings_toggled)
        self.news_clippings_toggled(self.cfg_news_clippings_checkbox.checkState())
        self.cfg_news_clippings_lineEdit.editingFinished.connect(self.news_clippings_destination_changed)

        # Launch the annotated_books_scanner
        field = get_cc_mapping('annotations', 'field', 'Comments')
        self.annotated_books_scanner = InventoryAnnotatedBooks(self.gui, field)
        self.annotated_books_scanner.signal.connect(self.inventory_complete)
#        self.connect(self.annotated_books_scanner, self.annotated_books_scanner.signal,
#            self.inventory_complete)
        QTimer.singleShot(1, self.start_inventory)

    def annotations_destination_changed(self, qs_new_destination_name):
        '''
        If the destination field changes, move all existing annotations from old to new
        '''
        self._log_location(repr(qs_new_destination_name))
        self._log("self.custom_fields: %s" % self.custom_fields)

        old_destination_field = get_cc_mapping('annotations', 'field', None)
        if old_destination_field and not (old_destination_field in self.gui.current_db.custom_field_keys() or old_destination_field == 'Comments'):
            return
        old_destination_name = get_cc_mapping('annotations', 'combobox', None)

        self._log("old_destination_field: %s" % old_destination_field)
        self._log("old_destination_name: %s" % old_destination_name)

        # Catch initial change from None to Comments - first run only
        if old_destination_field is None:
            return

#        new_destination_name = unicode(qs_new_destination_name)
        new_destination_name = unicode(self.cfg_annotations_destination_comboBox.currentText())
        self._log("new_destination_name: %s" % new_destination_name)

        if old_destination_name == new_destination_name:
            self._log_location("old_destination_name = new_destination_name, no changes")
            return

        new_destination_field = None
        if new_destination_name == 'Comments':
            new_destination_field = 'Comments'
        else:
            new_destination_field = self.custom_fields[new_destination_name]['field']

        if existing_annotations(self.opts.parent, old_destination_field):
            command = self.launch_new_destination_dialog(old_destination_name, new_destination_name)

            if command == 'move':
                set_cc_mapping('annotations', field=new_destination_field, combobox=new_destination_name)

                if self.annotated_books_scanner.isRunning():
                    self.annotated_books_scanner.wait()
                move_annotations(self, self.annotated_books_scanner.annotation_map,
                    old_destination_field, new_destination_field)

            elif command == 'change':
                # Keep the updated destination field, but don't move annotations
                pass

            elif command == 'cancel':
                # Restore previous destination
                self.cfg_annotations_destination_comboBox.blockSignals(True)
                old_index = self.cfg_annotations_destination_comboBox.findText(old_destination_name)
                self.cfg_annotations_destination_comboBox.setCurrentIndex(old_index)
                self.cfg_annotations_destination_comboBox.blockSignals(False)

            """
            # Warn user that change will move existing annotations to new field
            title = 'Move annotations?'
            msg = ("<p>Existing annotations will be moved from <b>%s</b> to <b>%s</b>.</p>" %
                    (old_destination_name, new_destination_name) +
                   "<p>New annotations will be added to <b>%s</b>.</p>" %
                    new_destination_name +
                   "<p>Proceed?</p>")
            d = MessageBox(MessageBox.QUESTION,
                           title, msg,
                           show_copy_button=False)
            self._log_location("QUESTION: %s" % msg)
            if d.exec_():
                set_cc_mapping('annotations', field=new_destination_field, combobox=new_destination_name)

                if self.annotated_books_scanner.isRunning():
                    self.annotated_books_scanner.wait()
                move_annotations(self, self.annotated_books_scanner.annotation_map,
                    old_destination_field, new_destination_field)

            else:
                self.cfg_annotations_destination_comboBox.blockSignals(True)
                old_index = self.cfg_annotations_destination_comboBox.findText(old_destination_name)
                self.cfg_annotations_destination_comboBox.setCurrentIndex(old_index)
                self.cfg_annotations_destination_comboBox.blockSignals(False)
            """

        else:
            # No existing annotations, just update prefs
            set_cc_mapping('annotations', field=new_destination_field, combobox=new_destination_name)

    def configure_appearance(self):
        '''
        '''
        from calibre_plugins.annotations.appearance import default_elements
        from calibre_plugins.annotations.appearance import default_timestamp
        appearance_settings = {
                                'appearance_css': default_elements,
                                'appearance_hr_checkbox': False,
                                'appearance_timestamp_format': default_timestamp
                              }

        # Save, hash the original settings
        original_settings = {}
        osh = hashlib.md5()
        for setting in appearance_settings:
            original_settings[setting] = plugin_prefs.get(setting, appearance_settings[setting])
            osh.update(repr(plugin_prefs.get(setting, appearance_settings[setting])))

        # Display the appearance dialog
        aa = AnnotationsAppearance(self, get_icon('images/annotations.png'), plugin_prefs)
        cancelled = False
        if aa.exec_():
            # appearance_hr_checkbox and appearance_timestamp_format changed live to prefs during previews
            plugin_prefs.set('appearance_css', aa.elements_table.get_data())
            # Generate a new hash
            nsh = hashlib.md5()
            for setting in appearance_settings:
                nsh.update(repr(plugin_prefs.get(setting, appearance_settings[setting])))
        else:
            for setting in appearance_settings:
                plugin_prefs.set(setting, original_settings[setting])
            nsh = osh

        # If there were changes, and there are existing annotations, offer to re-render
        field = get_cc_mapping('annotations', 'field', None)
        if osh.digest() != nsh.digest() and existing_annotations(self.opts.parent,field):
            title = _('Update annotations?')
            msg = _('<p>Update existing annotations to new appearance settings?</p>')
            d = MessageBox(MessageBox.QUESTION,
                           title, msg,
                           show_copy_button=False)
            self._log_location("QUESTION: %s" % msg)
            if d.exec_():
                self._log_location("Updating existing annotations to modified appearance")
                if self.annotated_books_scanner.isRunning():
                    self.annotated_books_scanner.wait()
                move_annotations(self, self.annotated_books_scanner.annotation_map,
                    field, field, window_title=_("Updating appearance"))

    def inventory_complete(self, msg):
        self._log_location(msg)

    def launch_cc_wizard(self, column_type):
        '''
        '''
        def _update_combo_box(comboBox, destination, previous):
            '''
            '''
            self._log_location()

            cb = getattr(self, comboBox)
            cb.blockSignals(True)
            all_items = [str(cb.itemText(i))
                         for i in range(cb.count())]
            if previous and previous in all_items:
                all_items.remove(previous)
            all_items.append(destination)

            cb.clear()
            cb.addItems(sorted(all_items, key=lambda s: s.lower()))

            # Select the new destination in the comboBox
            idx = cb.findText(destination)
            if idx > -1:
                cb.setCurrentIndex(idx)

            # Process the changed destination
            self.annotations_destination_changed(destination)

            cb.blockSignals(False)


        klass = os.path.join(dialog_resources_path, 'cc_wizard.py')
        if os.path.exists(klass):
            #self._log("importing CC Wizard dialog from '%s'" % klass)
            sys.path.insert(0, dialog_resources_path)
            this_dc = importlib.import_module('cc_wizard')
            sys.path.remove(dialog_resources_path)
            dlg = this_dc.CustomColumnWizard(self,
                                             column_type,
                                             self.WIZARD_PROFILES[column_type],
                                             verbose=True)
            dlg.exec_()

            if dlg.modified_column:
                self._log("modified_column: %s" % dlg.modified_column)

                destination = dlg.modified_column['destination']
                label = dlg.modified_column['label']
                previous = dlg.modified_column['previous']
                source = dlg.modified_column['source']

                self._log("destination: %s" % destination)
                self._log("label: %s" % label)
                self._log("previous: %s" % previous)
                self._log("source: %s" % source)

                if source == "Annotations":
                    # Add/update the new destination so save_settings() can find it
                    if destination in self.custom_fields:
                        self.custom_fields[destination]['field'] = label
                    else:
                        self.custom_fields[destination] = {'field': label}

                    _update_combo_box('cfg_annotations_destination_comboBox', destination, previous)

                    # Save field manually in case user cancels
                    #self.prefs.set('cfg_annotations_destination_comboBox', destination)
                    #self.prefs.set('cfg_annotations_destination_field', label)
                    set_cc_mapping('annotations', field=label, combobox=destination)

                    # Inform user to restart
                    self.restart_required('custom_column')

        else:
            self._log("ERROR: Can't import from '%s'" % klass)

    def launch_new_destination_dialog(self, old, new):
        '''
        Return 'move', 'change' or 'cancel'
        '''
        self._log_location()

        klass = os.path.join(dialog_resources_path, 'new_destination.py')
        if os.path.exists(klass):
            self._log("importing new destination dialog from '%s'" % klass)
            sys.path.insert(0, dialog_resources_path)
            this_dc = importlib.import_module('new_destination')
            sys.path.remove(dialog_resources_path)
            dlg = this_dc.NewDestinationDialog(self, old, new)
            dlg.exec_()
            return dlg.command

    def news_clippings_destination_changed(self):
        qs_new_destination_name = self.cfg_news_clippings_lineEdit.text()
        if not re.match(r'^\S+[A-Za-z0-9 ]+$', qs_new_destination_name):
            # Complain about News clippings title
            title = _('Invalid title for News clippings')
            msg = _("Supply a valid title for News clippings, for example 'My News Clippings'.")
            d = MessageBox(MessageBox.WARNING,
                           title, msg,
                           show_copy_button=False)
            self._log_location("WARNING: %s" % msg)
            d.exec_()

    def news_clippings_toggled(self, state):
        if state == Qt.Checked:
            self.cfg_news_clippings_lineEdit.setEnabled(True)
        else:
            self.cfg_news_clippings_lineEdit.setEnabled(False)

    def populate_annotations(self):
        '''
        Restore annotations combobox
        '''
        self._log_location()
        target = 'Comments'
        existing = get_cc_mapping('annotations', 'combobox')
        if existing:
            target = existing
        ci = self.cfg_annotations_destination_comboBox.findText(target)
        self.cfg_annotations_destination_comboBox.setCurrentIndex(ci)

    def restart_required(self, state):
        title = _('Restart required')
        msg = _('To apply changes, restart calibre.')
        d = MessageBox(MessageBox.WARNING,
                       title, msg,
                       show_copy_button=False)
        self._log_location("WARNING: %s" % (msg))
        d.exec_()

    def save_settings(self):
        save_state(self)

        # Save the annotation destination field
        ann_dest = unicode(self.cfg_annotations_destination_comboBox.currentText())
        self._log_location("INFO: ann_dest=%s" % (ann_dest))
        self._log_location("INFO: self.custom_fields=%s" % (self.custom_fields))
        if ann_dest == 'Comments':
            set_cc_mapping('annotations', field='Comments', combobox='Comments')
        elif ann_dest:
            set_cc_mapping('annotations', field=self.custom_fields[ann_dest]['field'], combobox=ann_dest)

    def start_inventory(self):
        self.annotated_books_scanner.start()