Exemple #1
0
class Dispatcher(QObject):
    '''
    Convenience class to use Qt signals with arbitrary python callables.
    By default, ensures that a function call always happens in the
    thread this Dispatcher was created in.

    Note that if you create the Dispatcher in a thread without an event loop of
    its own, the function call will happen in the GUI thread (I think).
    '''
    dispatch_signal = pyqtSignal(object, object)

    def __init__(self, func, queued=True, parent=None):
        QObject.__init__(self, parent)
        self.func = func
        typ = Qt.QueuedConnection
        if not queued:
            typ = Qt.AutoConnection if queued is None else Qt.DirectConnection
        self.dispatch_signal.connect(self.dispatch, type=typ)

    def __call__(self, *args, **kwargs):
        self.dispatch_signal.emit(args, kwargs)

    def dispatch(self, args, kwargs):
        self.func(*args, **kwargs)
Exemple #2
0
class UnitedStates(QObject):
    library_changed = pyqtSignal()

    def __init__(self):
        QObject.__init__(self)
        self.edit_stamp = datetime.datetime(2013, 1, 11, 0, 0, 0, 0)
        tmp_lsb = os.path.join(tempfile.gettempdir(), "lsb")
        if os.path.exists(tmp_lsb):
            shutil.rmtree(tmp_lsb, ignore_errors=True)
        self.portable_directory = tmp_lsb
        # self.portable_directory = "windows_logs"
        self.plugin_url = ('https://github.com/marcellmars/'
                           'letssharebooks/raw/master/calibreletssharebooks/'
                           'letssharebooks_calibre.zip')
        self.running_version = ".".join(map(str, lsb.version))
        try:
            self.latest_version = urllib2.urlopen(
                'https://raw.github.com/marcellmars/letssharebooks/master/'
                'calibreletssharebooks/_version').read()[:-1].encode("utf-8")
        except:
            self.latest_version = "0.0.0"

    def library_changed_emit(self):
        self.library_changed.emit()
Exemple #3
0
class TOCViewer(QWidget):

    navigate_requested = pyqtSignal(object, object)

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QGridLayout(self)
        self.setLayout(l)
        l.setContentsMargins(0, 0, 0, 0)

        self.is_visible = False
        self.view = QTreeWidget(self)
        self.delegate = Delegate(self.view)
        self.view.setItemDelegate(self.delegate)
        self.view.setHeaderHidden(True)
        self.view.setAnimated(True)
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.show_context_menu,
                                                     type=Qt.QueuedConnection)
        self.view.itemActivated.connect(self.emit_navigate)
        self.view.itemPressed.connect(self.item_pressed)
        pi = plugins['progress_indicator'][0]
        if hasattr(pi, 'set_no_activate_on_click'):
            pi.set_no_activate_on_click(self.view)
        self.view.itemDoubleClicked.connect(self.emit_navigate)
        l.addWidget(self.view)

        self.refresh_action = QAction(QIcon(I('view-refresh.png')),
                                      _('&Refresh'), self)
        self.refresh_action.triggered.connect(self.build)

    def item_pressed(self, item):
        if QApplication.mouseButtons() & Qt.LeftButton:
            QTimer.singleShot(0, self.emit_navigate)

    def show_context_menu(self, pos):
        menu = QMenu(self)
        menu.addAction(actions['edit-toc'])
        menu.addAction(_('&Expand all'), self.view.expandAll)
        menu.addAction(_('&Collapse all'), self.view.collapseAll)
        menu.addAction(self.refresh_action)
        menu.exec_(self.view.mapToGlobal(pos))

    def iteritems(self, parent=None):
        if parent is None:
            parent = self.invisibleRootItem()
        for i in xrange(parent.childCount()):
            child = parent.child(i)
            yield child
            for gc in self.iteritems(parent=child):
                yield gc

    def emit_navigate(self, *args):
        item = self.view.currentItem()
        if item is not None:
            dest = unicode(item.data(0, DEST_ROLE).toString())
            frag = unicode(item.data(0, FRAG_ROLE).toString())
            if not frag:
                frag = TOP
            self.navigate_requested.emit(dest, frag)

    def build(self):
        c = current_container()
        if c is None:
            return
        toc = get_toc(c, verify_destinations=False)

        def process_node(toc, parent):
            for child in toc:
                node = QTreeWidgetItem(parent)
                node.setText(0, child.title or '')
                node.setData(0, DEST_ROLE, child.dest or '')
                node.setData(0, FRAG_ROLE, child.frag or '')
                tt = _('File: {0}\nAnchor: {1}').format(
                    child.dest or '', child.frag or _('Top of file'))
                node.setData(0, Qt.ToolTipRole, tt)
                process_node(child, node)

        self.view.clear()
        process_node(toc, self.view.invisibleRootItem())

    def visibility_changed(self, visible):
        self.is_visible = visible
        if visible:
            self.build()

    def update_if_visible(self):
        if self.is_visible:
            self.build()
Exemple #4
0
class GridView(QListView):

    update_item = pyqtSignal(object)
    files_dropped = pyqtSignal(object)

    def __init__(self, parent):
        QListView.__init__(self, parent)
        setup_dnd_interface(self)
        self.setUniformItemSizes(True)
        self.setWrapping(True)
        self.setFlow(self.LeftToRight)
        # We cannot set layout mode to batched, because that breaks
        # restore_vpos()
        # self.setLayoutMode(self.Batched)
        self.setResizeMode(self.Adjust)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollMode(self.ScrollPerPixel)
        self.delegate = CoverDelegate(self)
        self.delegate.animation.valueChanged.connect(
            self.animation_value_changed)
        self.delegate.animation.finished.connect(self.animation_done)
        self.setItemDelegate(self.delegate)
        self.setSpacing(self.delegate.spacing)
        self.padding_left = 0
        self.set_color()
        self.ignore_render_requests = Event()
        self.thumbnail_cache = ThumbnailCache(
            max_size=gprefs['cover_grid_disk_cache_size'],
            thumbnail_size=(self.delegate.cover_size.width(),
                            self.delegate.cover_size.height()))
        self.render_thread = None
        self.update_item.connect(self.re_render, type=Qt.QueuedConnection)
        self.doubleClicked.connect(self.double_clicked)
        self.setCursor(Qt.PointingHandCursor)
        self.gui = parent
        self.context_menu = None
        self.update_timer = QTimer(self)
        self.update_timer.setInterval(200)
        self.update_timer.timeout.connect(self.update_viewport)
        self.update_timer.setSingleShot(True)

    @property
    def first_visible_row(self):
        geom = self.viewport().geometry()
        for y in xrange(geom.top(), (self.spacing() * 2) + geom.top(), 5):
            for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(),
                            5):
                ans = self.indexAt(QPoint(x, y)).row()
                if ans > -1:
                    return ans

    @property
    def last_visible_row(self):
        geom = self.viewport().geometry()
        for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5):
            for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(),
                            5):
                ans = self.indexAt(QPoint(x, y)).row()
                if ans > -1:
                    item_width = self.delegate.item_size.width(
                    ) + 2 * self.spacing()
                    return ans + (geom.width() // item_width)

    def update_viewport(self):
        self.ignore_render_requests.clear()
        self.update_timer.stop()
        m = self.model()
        for r in xrange(self.first_visible_row or 0, self.last_visible_row
                        or (m.count() - 1)):
            self.update(m.index(r, 0))

    def double_clicked(self, index):
        d = self.delegate
        if d.animating is None and not config['disable_animations']:
            d.animating = index
            d.animation.start()
        if tweaks['doubleclick_on_library_view'] == 'open_viewer':
            self.gui.iactions['View'].view_triggered(index)
        elif tweaks['doubleclick_on_library_view'] in {
                'edit_metadata', 'edit_cell'
        }:
            self.gui.iactions['Edit Metadata'].edit_metadata(False, False)

    def animation_value_changed(self, value):
        if self.delegate.animating is not None:
            self.update(self.delegate.animating)

    def animation_done(self):
        if self.delegate.animating is not None:
            idx = self.delegate.animating
            self.delegate.animating = None
            self.update(idx)

    def set_color(self):
        r, g, b = gprefs['cover_grid_color']
        pal = QPalette()
        col = QColor(r, g, b)
        pal.setColor(pal.Base, col)
        tex = gprefs['cover_grid_texture']
        if tex:
            from calibre.gui2.preferences.texture_chooser import texture_path
            path = texture_path(tex)
            if path:
                pm = QPixmap(path)
                if not pm.isNull():
                    val = pm.scaled(1, 1).toImage().pixel(0, 0)
                    r, g, b = qRed(val), qGreen(val), qBlue(val)
                    pal.setBrush(pal.Base, QBrush(pm))
        dark = (r + g + b) / 3.0 < 128
        pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black))
        self.setPalette(pal)
        self.delegate.highlight_color = pal.color(pal.Text)

    def refresh_settings(self):
        size_changed = (
            gprefs['cover_grid_width'] != self.delegate.original_width
            or gprefs['cover_grid_height'] != self.delegate.original_height)
        if (size_changed or gprefs['cover_grid_show_title'] !=
                self.delegate.original_show_title):
            self.delegate.set_dimensions()
            self.setSpacing(self.delegate.spacing)
            if size_changed:
                self.delegate.cover_cache.clear()
        if gprefs['cover_grid_spacing'] != self.delegate.original_spacing:
            self.delegate.calculate_spacing()
            self.setSpacing(self.delegate.spacing)
        self.set_color()
        self.delegate.cover_cache.set_limit(gprefs['cover_grid_cache_size'])
        if size_changed:
            self.thumbnail_cache.set_thumbnail_size(
                self.delegate.cover_size.width(),
                self.delegate.cover_size.height())
        cs = gprefs['cover_grid_disk_cache_size']
        if (cs * (1024**2)) != self.thumbnail_cache.max_size:
            self.thumbnail_cache.set_size(cs)

    def shown(self):
        if self.render_thread is None:
            self.thumbnail_cache.set_database(self.gui.current_db)
            self.render_thread = Thread(target=self.render_covers)
            self.render_thread.daemon = True
            self.render_thread.start()

    def render_covers(self):
        q = self.delegate.render_queue
        while True:
            book_id = q.get()
            try:
                if book_id is None:
                    return
                if self.ignore_render_requests.is_set():
                    continue
                try:
                    self.render_cover(book_id)
                except:
                    import traceback
                    traceback.print_exc()
            finally:
                q.task_done()

    def render_cover(self, book_id):
        if self.ignore_render_requests.is_set():
            return
        tcdata, timestamp = self.thumbnail_cache[book_id]
        use_cache = False
        if timestamp is None:
            # Not in cache
            has_cover, cdata, timestamp = self.model(
            ).db.new_api.cover_or_cache(book_id, 0)
        else:
            has_cover, cdata, timestamp = self.model(
            ).db.new_api.cover_or_cache(book_id, timestamp)
            if has_cover and cdata is None:
                # The cached cover is fresh
                cdata = tcdata
                use_cache = True

        if has_cover:
            p = QImage()
            p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG')
            if p.isNull() and cdata is tcdata:
                # Invalid image in cache
                self.thumbnail_cache.invalidate((book_id, ))
                self.update_item.emit(book_id)
                return
            cdata = None if p.isNull() else p
            if not use_cache:  # cache is stale
                if cdata is not None:
                    width, height = p.width(), p.height()
                    scaled, nwidth, nheight = fit_image(
                        width, height, self.delegate.cover_size.width(),
                        self.delegate.cover_size.height())
                    if scaled:
                        if self.ignore_render_requests.is_set():
                            return
                        p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio,
                                     Qt.SmoothTransformation)
                    cdata = p
                # update cache
                if cdata is None:
                    self.thumbnail_cache.invalidate((book_id, ))
                else:
                    try:
                        self.thumbnail_cache.insert(book_id, timestamp,
                                                    image_to_data(cdata))
                    except EncodeError as err:
                        self.thumbnail_cache.invalidate((book_id, ))
                        prints(err)
                    except Exception:
                        import traceback
                        traceback.print_exc()
        elif tcdata is not None:
            # Cover was removed, but it exists in cache, remove from cache
            self.thumbnail_cache.invalidate((book_id, ))
        self.delegate.cover_cache.set(book_id, cdata)
        self.update_item.emit(book_id)

    def re_render(self, book_id):
        self.delegate.cover_cache.clear_staging()
        m = self.model()
        try:
            index = m.db.row(book_id)
        except (IndexError, ValueError, KeyError):
            return
        self.update(m.index(index, 0))

    def shutdown(self):
        self.ignore_render_requests.set()
        self.delegate.render_queue.put(None)
        self.thumbnail_cache.shutdown()

    def set_database(self, newdb, stage=0):
        if stage == 0:
            self.ignore_render_requests.set()
            try:
                for x in (self.delegate.cover_cache, self.thumbnail_cache):
                    self.model().db.new_api.remove_cover_cache(x)
            except AttributeError:
                pass  # db is None
            for x in (self.delegate.cover_cache, self.thumbnail_cache):
                newdb.new_api.add_cover_cache(x)
            try:
                # Use a timeout so that if, for some reason, the render thread
                # gets stuck, we dont deadlock, future covers wont get
                # rendered, but this is better than a deadlock
                join_with_timeout(self.delegate.render_queue)
            except RuntimeError:
                print('Cover rendering thread is stuck!')
            finally:
                self.ignore_render_requests.clear()
        else:
            self.delegate.cover_cache.clear()

    def select_rows(self, rows):
        sel = QItemSelection()
        sm = self.selectionModel()
        m = self.model()
        # Create a range based selector for each set of contiguous rows
        # as supplying selectors for each individual row causes very poor
        # performance if a large number of rows has to be selected.
        for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(
                QItemSelection(m.index(min(group), 0), m.index(max(group), 0)),
                sm.Select)
        sm.select(sel, sm.ClearAndSelect)
Exemple #5
0
class CoverDelegate(QStyledItemDelegate):  # {{{

    needs_redraw = pyqtSignal()

    def __init__(self, parent):
        QStyledItemDelegate.__init__(self, parent)

        self.angle = 0
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.frame_changed)
        self.color = parent.palette().color(QPalette.WindowText)
        self.spinner_width = 64

    def frame_changed(self, *args):
        self.angle = (self.angle + 30) % 360
        self.needs_redraw.emit()

    def start_animation(self):
        self.angle = 0
        self.timer.start(200)

    def stop_animation(self):
        self.timer.stop()

    def draw_spinner(self, painter, rect):
        width = rect.width()

        outer_radius = (width - 1) * 0.5
        inner_radius = (width - 1) * 0.5 * 0.38

        capsule_height = outer_radius - inner_radius
        capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35))
        capsule_radius = capsule_width // 2

        painter.save()
        painter.setRenderHint(painter.Antialiasing)

        for i in xrange(12):
            color = QColor(self.color)
            color.setAlphaF(1.0 - (i / 12.0))
            painter.setPen(Qt.NoPen)
            painter.setBrush(color)
            painter.save()
            painter.translate(rect.center())
            painter.rotate(self.angle - i * 30.0)
            painter.drawRoundedRect(-capsule_width * 0.5,
                                    -(inner_radius + capsule_height),
                                    capsule_width, capsule_height,
                                    capsule_radius, capsule_radius)
            painter.restore()
        painter.restore()

    def paint(self, painter, option, index):
        QStyledItemDelegate.paint(self, painter, option, index)
        style = QApplication.style()
        waiting = self.timer.isActive() and index.data(Qt.UserRole).toBool()
        if waiting:
            rect = QRect(0, 0, self.spinner_width, self.spinner_width)
            rect.moveCenter(option.rect.center())
            self.draw_spinner(painter, rect)
        else:
            # Ensure the cover is rendered over any selection rect
            style.drawItemPixmap(painter, option.rect,
                                 Qt.AlignTop | Qt.AlignHCenter,
                                 QPixmap(index.data(Qt.DecorationRole)))
Exemple #6
0
class IdentifyWidget(QWidget):  # {{{

    rejected = pyqtSignal()
    results_found = pyqtSignal()
    book_selected = pyqtSignal(object, object)

    def __init__(self, log, parent=None):
        QWidget.__init__(self, parent)
        self.log = log
        self.abort = Event()
        self.caches = {}

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

        names = [
            '<b>' + p.name + '</b>' for p in metadata_plugins(['identify'])
            if p.is_configured()
        ]
        self.top = QLabel('<p>' + _('calibre is downloading metadata from: ') +
                          ', '.join(names))
        self.top.setWordWrap(True)
        l.addWidget(self.top, 0, 0)

        self.results_view = ResultsView(self)
        self.results_view.book_selected.connect(self.emit_book_selected)
        self.get_result = self.results_view.get_result
        l.addWidget(self.results_view, 1, 0)

        self.comments_view = Comments(self)
        l.addWidget(self.comments_view, 1, 1)

        self.results_view.show_details_signal.connect(
            self.comments_view.show_data)

        self.query = QLabel('download starting...')
        self.query.setWordWrap(True)
        l.addWidget(self.query, 2, 0, 1, 2)

        self.comments_view.show_data('<h2>' + _('Please wait') +
                                     '<br><span id="dots">.</span></h2>' + '''
                <script type="text/javascript">
                window.onload=function(){
                    var dotspan = document.getElementById('dots');
                    window.setInterval(function(){
                        if(dotspan.textContent == '............'){
                        dotspan.textContent = '.';
                        }
                        else{
                        dotspan.textContent += '.';
                        }
                    }, 400);
                }
                </script>
                ''')

    def emit_book_selected(self, book):
        self.book_selected.emit(book, self.caches)

    def start(self, title=None, authors=None, identifiers={}):
        self.log.clear()
        self.log('Starting download')
        parts, simple_desc = [], ''
        if title:
            parts.append('title:' + title)
            simple_desc += _('Title: %s ') % title
        if authors:
            parts.append('authors:' + authors_to_string(authors))
            simple_desc += _('Authors: %s ') % authors_to_string(authors)
        if identifiers:
            x = ', '.join('%s:%s' % (k, v) for k, v in identifiers.iteritems())
            parts.append(x)
            if 'isbn' in identifiers:
                simple_desc += ' ISBN: %s' % identifiers['isbn']
        self.query.setText(simple_desc)
        self.log(unicode(self.query.text()))

        self.worker = IdentifyWorker(self.log, self.abort, title, authors,
                                     identifiers, self.caches)

        self.worker.start()

        QTimer.singleShot(50, self.update)

    def update(self):
        if self.worker.is_alive():
            QTimer.singleShot(50, self.update)
        else:
            self.process_results()

    def process_results(self):
        if self.worker.error is not None:
            error_dialog(self,
                         _('Download failed'),
                         _('Failed to download metadata. Click '
                           'Show Details to see details'),
                         show=True,
                         det_msg=self.worker.error)
            self.rejected.emit()
            return

        if not self.worker.results:
            log = ''.join(self.log.plain_text)
            error_dialog(
                self,
                _('No matches found'),
                '<p>' +
                _('Failed to find any books that '
                  'match your search. Try making the search <b>less '
                  'specific</b>. For example, use only the author\'s '
                  'last name and a single distinctive word from '
                  'the title.<p>To see the full log, click Show Details.'),
                show=True,
                det_msg=log)
            self.rejected.emit()
            return

        self.results_view.show_results(self.worker.results)
        self.results_found.emit()

    def cancel(self):
        self.abort.set()
Exemple #7
0
class ChooseLibraryAction(InterfaceAction):

    name = 'Choose Library'
    action_spec = (_('Choose Library'), 'lt.png',
                   _('Choose calibre library to work with'), None)
    dont_add_to = frozenset(['context-menu-device'])
    action_add_menu = True
    action_menu_clone_qaction = _('Switch/create library...')
    restore_view_state = pyqtSignal(object)

    def genesis(self):
        self.count_changed(0)
        self.action_choose = self.menuless_qaction

        self.stats = LibraryUsageStats()
        self.popup_type = (QToolButton.InstantPopup
                           if len(self.stats.stats) > 1 else
                           QToolButton.MenuButtonPopup)
        if len(self.stats.stats) > 1:
            self.action_choose.triggered.connect(self.choose_library)
        else:
            self.qaction.triggered.connect(self.choose_library)

        self.choose_menu = self.qaction.menu()

        ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
                                      None, None),
                                attr='action_pick_random')
        ac.triggered.connect(self.pick_random)

        if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            self.choose_menu.addAction(self.action_choose)

            self.quick_menu = QMenu(_('Quick switch'))
            self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
            self.rename_menu = QMenu(_('Rename library'))
            self.rename_menu_action = self.choose_menu.addMenu(
                self.rename_menu)
            self.choose_menu.addAction(ac)
            self.delete_menu = QMenu(_('Remove library'))
            self.delete_menu_action = self.choose_menu.addMenu(
                self.delete_menu)
        else:
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.switch_actions = []
        for i in range(5):
            ac = self.create_action(spec=('', None, None, None),
                                    attr='switch_action%d' % i)
            self.switch_actions.append(ac)
            ac.setVisible(False)
            ac.triggered.connect(partial(self.qs_requested, i),
                                 type=Qt.QueuedConnection)
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.maintenance_menu = QMenu(_('Library Maintenance'))
        ac = self.create_action(spec=(_('Library metadata backup status'),
                                      'lt.png', None, None),
                                attr='action_backup_status')
        ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Check library'), 'lt.png', None,
                                      None),
                                attr='action_check_library')
        ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Restore database'), 'lt.png', None,
                                      None),
                                attr='action_restore_database')
        ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)

        self.choose_menu.addMenu(self.maintenance_menu)
        self.view_state_map = {}
        self.restore_view_state.connect(self._restore_view_state,
                                        type=Qt.QueuedConnection)

    @property
    def preserve_state_on_switch(self):
        ans = getattr(self, '_preserve_state_on_switch', None)
        if ans is None:
            self._preserve_state_on_switch = ans = \
                self.gui.library_view.preserve_state(require_selected_ids=False)
        return ans

    def pick_random(self, *args):
        self.gui.iactions['Pick Random Book'].pick_random()

    def library_name(self):
        db = self.gui.library_view.model().db
        path = db.library_path
        if isbytestring(path):
            path = path.decode(filesystem_encoding)
        path = path.replace(os.sep, '/')
        return self.stats.pretty(path)

    def update_tooltip(self, count):
        tooltip = self.action_spec[2] + '\n\n' + _('{0} [{1} books]').format(
            getattr(self, 'last_lname', ''), count)
        a = self.qaction
        a.setToolTip(tooltip)
        a.setStatusTip(tooltip)
        a.setWhatsThis(tooltip)

    def library_changed(self, db):
        lname = self.stats.library_used(db)
        self.last_lname = lname
        if len(lname) > 16:
            lname = lname[:16] + u'…'
        a = self.qaction
        a.setText(lname)
        self.update_tooltip(db.count())
        self.build_menus()
        state = self.view_state_map.get(
            self.stats.canonicalize_path(db.library_path), None)
        if state is not None:
            self.restore_view_state.emit(state)

    def _restore_view_state(self, state):
        self.preserve_state_on_switch.state = state

    def initialization_complete(self):
        self.library_changed(self.gui.library_view.model().db)

    def build_menus(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            return
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))

        for ac in self.switch_actions:
            ac.setVisible(False)
        self.quick_menu.clear()
        self.qs_locations = [i[1] for i in locations]
        self.rename_menu.clear()
        self.delete_menu.clear()
        quick_actions, rename_actions, delete_actions = [], [], []
        for name, loc in locations:
            ac = self.quick_menu.addAction(
                name, Dispatcher(partial(self.switch_requested, loc)))
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(
                name, Dispatcher(partial(self.rename_requested, name, loc)))
            rename_actions.append(ac)
            ac = self.delete_menu.addAction(
                name, Dispatcher(partial(self.delete_requested, name, loc)))
            delete_actions.append(ac)

        qs_actions = []
        for i, x in enumerate(locations[:len(self.switch_actions)]):
            name, loc = x
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setVisible(True)
            qs_actions.append(ac)

        self.quick_menu_action.setVisible(bool(locations))
        self.rename_menu_action.setVisible(bool(locations))
        self.delete_menu_action.setVisible(bool(locations))
        self.gui.location_manager.set_switch_actions(quick_actions,
                                                     rename_actions,
                                                     delete_actions,
                                                     qs_actions,
                                                     self.action_choose)

    def location_selected(self, loc):
        enabled = loc == 'library'
        self.qaction.setEnabled(enabled)

    def rename_requested(self, name, location):
        LibraryDatabase = db_class()
        loc = location.replace('/', os.sep)
        base = os.path.dirname(loc)
        newname, ok = QInputDialog.getText(
            self.gui,
            _('Rename') + ' ' + name,
            '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name +
            '<p>' + _('Note that the actual library folder will be renamed.'),
            text=name)
        newname = sanitize_file_name_unicode(unicode(newname))
        if not ok or not newname or newname == name:
            return
        newloc = os.path.join(base, newname)
        if os.path.exists(newloc):
            return error_dialog(
                self.gui,
                _('Already exists'),
                _('The folder %s already exists. Delete it first.') % newloc,
                show=True)
        if (iswindows
                and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(self.gui,
                                _('Too long'),
                                _('Path to library too long. Must be less than'
                                  ' %d characters.') %
                                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                                show=True)
        if not os.path.exists(loc):
            error_dialog(
                self.gui,
                _('Not found'),
                _('Cannot rename as no library was found at %s. '
                  'Try switching to this library first, then switch back '
                  'and retry the renaming.') % loc,
                show=True)
            return
        try:
            os.rename(loc, newloc)
        except:
            import traceback
            det_msg = 'Location: %r New Location: %r\n%s' % (
                loc, newloc, traceback.format_exc())
            error_dialog(
                self.gui,
                _('Rename failed'),
                _('Failed to rename the library at %s. '
                  'The most common cause for this is if one of the files'
                  ' in the library is open in another program.') % loc,
                det_msg=det_msg,
                show=True)
            return
        self.stats.rename(location, newloc)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def delete_requested(self, name, location):
        loc = location.replace('/', os.sep)
        self.stats.remove(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        info_dialog(self.gui,
                    _('Library removed'),
                    _('The library %s has been removed from calibre. '
                      'The files remain on your computer, if you want '
                      'to delete them, you will have to do so manually.') %
                    loc,
                    show=True)
        if os.path.exists(loc):
            open_local_file(loc)

    def backup_status(self, location):
        self.__backup_status_dialog = d = BackupStatus(self.gui)
        d.show()

    def mark_dirty(self):
        db = self.gui.library_view.model().db
        db.dirtied(list(db.data.iterallids()))
        info_dialog(
            self.gui,
            _('Backup metadata'),
            _('Metadata will be backed up while calibre is running, at the '
              'rate of approximately 1 book every three seconds.'),
            show=True)

    def restore_database(self):
        LibraryDatabase = db_class()
        m = self.gui.library_view.model()
        db = m.db
        if (iswindows and len(db.library_path) >
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(
                self.gui,
                _('Too long'),
                _('Path to library too long. Must be less than'
                  ' %d characters. Move your library to a location with'
                  ' a shorter path using Windows Explorer, then point'
                  ' calibre to the new location and try again.') %
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                show=True)

        from calibre.gui2.dialogs.restore_library import restore_database
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True
        if restore_database(db, self.gui):
            self.gui.library_moved(db.library_path, call_close=False)

    def check_library(self):
        from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck, DBCheckNew
        self.gui.library_view.save_state()
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True

        if hasattr(db, 'new_api'):
            d = DBCheckNew(self.gui, db)
        else:
            d = DBCheck(self.gui, db)
        d.start()
        try:
            d.conn.close()
        except:
            pass
        d.break_cycles()
        self.gui.library_moved(db.library_path,
                               call_close=not d.closed_orig_conn)
        if d.rejected:
            return
        if d.error is None:
            if not question_dialog(
                    self.gui, _('Success'),
                    _('Found no errors in your calibre library database.'
                      ' Do you want calibre to check if the files in your '
                      ' library match the information in the database?')):
                return
        else:
            return error_dialog(
                self.gui,
                _('Failed'),
                _('Database integrity check failed, click Show details'
                  ' for details.'),
                show=True,
                det_msg=d.error[1])

        self.gui.status_bar.show_message(
            _('Starting library scan, this may take a while'))
        try:
            QCoreApplication.processEvents()
            d = CheckLibraryDialog(self.gui, m.db)

            if not d.do_exec():
                info_dialog(
                    self.gui,
                    _('No problems found'),
                    _('The files in your library match the information '
                      'in the database.'),
                    show=True)
        finally:
            self.gui.status_bar.clear_message()

    def look_for_portable_lib(self, db, location):
        base = get_portable_base()
        if base is None:
            return False, None
        loc = location.replace('/', os.sep)
        candidate = os.path.join(base, os.path.basename(loc))
        if db.exists_at(candidate):
            newloc = candidate.replace(os.sep, '/')
            self.stats.rename(location, newloc)
            return True, newloc
        return False, None

    def switch_requested(self, location):
        if not self.change_library_allowed():
            return
        db = self.gui.library_view.model().db
        current_lib = self.stats.canonicalize_path(db.library_path)
        self.view_state_map[current_lib] = self.preserve_state_on_switch.state
        loc = location.replace('/', os.sep)
        exists = db.exists_at(loc)
        if not exists:
            exists, new_location = self.look_for_portable_lib(db, location)
            if exists:
                location = new_location
                loc = location.replace('/', os.sep)

        if not exists:
            d = MovedDialog(self.stats, location, self.gui)
            ret = d.exec_()
            self.build_menus()
            self.gui.iactions['Copy To Library'].build_menus()
            if ret == d.Accepted:
                loc = d.newloc.replace('/', os.sep)
            else:
                return

        # from calibre.utils.mem import memory
        # import weakref
        # from PyQt4.Qt import QTimer
        # self.dbref = weakref.ref(self.gui.library_view.model().db)
        # self.before_mem = memory()
        self.gui.library_moved(loc, allow_rebuild=True)
        # QTimer.singleShot(5000, self.debug_leak)

    def debug_leak(self):
        import gc
        from calibre.utils.mem import memory
        ref = self.dbref
        for i in xrange(3):
            gc.collect()
        if ref() is not None:
            print 'DB object alive:', ref()
            for r in gc.get_referrers(ref())[:10]:
                print r
                print
        print 'before:', self.before_mem
        print 'after:', memory()
        print
        self.dbref = self.before_mem = None

    def qs_requested(self, idx, *args):
        self.switch_requested(self.qs_locations[idx])

    def count_changed(self, new_count):
        self.update_tooltip(new_count)

    def choose_library(self, *args):
        if not self.change_library_allowed():
            return
        from calibre.gui2.dialogs.choose_library import ChooseLibrary
        self.gui.library_view.save_state()
        db = self.gui.library_view.model().db
        location = self.stats.canonicalize_path(db.library_path)
        self.pre_choose_dialog_location = location
        c = ChooseLibrary(db, self.choose_library_callback, self.gui)
        c.exec_()
        self.choose_dialog_library_renamed = getattr(c, 'library_renamed',
                                                     False)

    def choose_library_callback(self, newloc, copy_structure=False):
        self.gui.library_moved(newloc,
                               copy_structure=copy_structure,
                               allow_rebuild=True)
        if getattr(self, 'choose_dialog_library_renamed', False):
            self.stats.rename(self.pre_choose_dialog_location,
                              prefs['library_path'])
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def change_library_allowed(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            warning_dialog(
                self.gui,
                _('Not allowed'),
                _('You cannot change libraries while using the environment'
                  ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'),
                show=True)
            return False
        if self.gui.job_manager.has_jobs():
            warning_dialog(self.gui,
                           _('Not allowed'),
                           _('You cannot change libraries while jobs'
                             ' are running.'),
                           show=True)
            return False

        return True
Exemple #8
0
class Completer(QListView):  # {{{

    item_selected = pyqtSignal(object)
    relayout_needed = pyqtSignal()

    def __init__(self, completer_widget, max_visible_items=7):
        QListView.__init__(self)
        self.completer_widget = weakref.ref(completer_widget)
        self.setWindowFlags(Qt.Popup)
        self.max_visible_items = max_visible_items
        self.setEditTriggers(self.NoEditTriggers)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.SingleSelection)
        self.setAlternatingRowColors(True)
        self.setModel(CompleteModel(self))
        self.setMouseTracking(True)
        self.entered.connect(self.item_entered)
        self.activated.connect(self.item_chosen)
        self.pressed.connect(self.item_chosen)
        self.installEventFilter(self)

    def hide(self):
        self.setCurrentIndex(QModelIndex())
        QListView.hide(self)

    def item_chosen(self, index):
        if not self.isVisible():
            return
        self.hide()
        text = self.model().data(index, Qt.DisplayRole)
        self.item_selected.emit(unicode(text))

    def set_items(self, items):
        self.model().set_items(items)
        if self.isVisible():
            self.relayout_needed.emit()

    def set_completion_prefix(self, prefix):
        self.model().set_completion_prefix(prefix)
        if self.isVisible():
            self.relayout_needed.emit()

    def item_entered(self, idx):
        self.setCurrentIndex(idx)

    def next_match(self, previous=False):
        c = self.currentIndex()
        if c.isValid():
            r = c.row()
        else:
            r = self.model().rowCount() if previous else -1
        r = r + (-1 if previous else 1)
        index = self.model().index(r % self.model().rowCount())
        self.setCurrentIndex(index)

    def scroll_to(self, orig):
        if orig:
            index = self.model().index_for_prefix(orig)
            if index is not None and index.isValid():
                self.setCurrentIndex(index)

    def popup(self, select_first=True):
        p = self
        m = p.model()
        widget = self.completer_widget()
        if widget is None:
            return
        screen = QApplication.desktop().availableGeometry(widget)
        h = (p.sizeHintForRow(0) * min(self.max_visible_items, m.rowCount()) +
             3) + 3
        hsb = p.horizontalScrollBar()
        if hsb and hsb.isVisible():
            h += hsb.sizeHint().height()

        rh = widget.height()
        pos = widget.mapToGlobal(QPoint(0, widget.height() - 2))
        w = min(widget.width(), screen.width())

        if (pos.x() + w) > (screen.x() + screen.width()):
            pos.setX(screen.x() + screen.width() - w)
        if pos.x() < screen.x():
            pos.setX(screen.x())

        top = pos.y() - rh - screen.top() + 2
        bottom = screen.bottom() - pos.y()
        h = max(h, p.minimumHeight())
        if h > bottom:
            h = min(max(top, bottom), h)

            if top > bottom:
                pos.setY(pos.y() - h - rh + 2)

        p.setGeometry(pos.x(), pos.y(), w, h)

        if (tweaks['preselect_first_completion'] and select_first
                and not self.currentIndex().isValid()
                and self.model().rowCount() > 0):
            self.setCurrentIndex(self.model().index(0))

        if not p.isVisible():
            if isosx and get_osx_version() >= (10, 9, 0):
                # On mavericks the popup menu seems to use a font smaller than
                # the widgets font, see for example:
                # https://bugs.launchpad.net/bugs/1243761
                fp = QFontInfo(widget.font())
                f = QFont()
                f.setPixelSize(fp.pixelSize())
                self.setFont(f)
            p.show()

    def eventFilter(self, obj, e):
        'Redirect key presses from the popup to the widget'
        widget = self.completer_widget()
        if widget is None or sip.isdeleted(widget):
            return False
        etype = e.type()
        if obj is not self:
            return QObject.eventFilter(self, obj, e)

        if etype == e.KeyPress:
            key = e.key()
            if key == Qt.Key_Escape:
                self.hide()
                e.accept()
                return True
            if key == Qt.Key_F4 and e.modifiers() & Qt.AltModifier:
                self.hide()
                e.accept()
                return True
            if key in (Qt.Key_Enter, Qt.Key_Return):
                # We handle this explicitly because on OS X activated() is
                # not emitted on pressing Enter.
                idx = self.currentIndex()
                if idx.isValid():
                    self.item_chosen(idx)
                self.hide()
                e.accept()
                return True
            if key == Qt.Key_Tab:
                idx = self.currentIndex()
                if idx.isValid():
                    self.item_chosen(idx)
                    self.hide()
                elif self.model().rowCount() > 0:
                    self.next_match()
                e.accept()
                return True
            if key in (Qt.Key_PageUp, Qt.Key_PageDown):
                # Let the list view handle these keys
                return False
            if key in (Qt.Key_Up, Qt.Key_Down):
                self.next_match(previous=key == Qt.Key_Up)
                e.accept()
                return True
            # Send to widget
            widget.eat_focus_out = False
            widget.keyPressEvent(e)
            widget.eat_focus_out = True
            if not widget.hasFocus():
                # Widget lost focus hide the popup
                self.hide()
            if e.isAccepted():
                return True
        elif etype == e.MouseButtonPress:
            if not self.underMouse():
                self.hide()
                e.accept()
                return True
        elif etype in (e.InputMethod, e.ShortcutOverride):
            QApplication.sendEvent(widget, e)
        return False
Exemple #9
0
class SavedSearches(Dialog):

    run_saved_searches = pyqtSignal(object, object)

    def __init__(self, parent=None):
        Dialog.__init__(self,
                        _('Saved Searches'),
                        'saved-searches',
                        parent=parent)

    def sizeHint(self):
        return QSize(800, 675)

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        self.setLayout(l)

        self.h = h = QHBoxLayout()
        self.filter_text = ft = QLineEdit(self)
        ft.textChanged.connect(self.do_filter)
        ft.setPlaceholderText(_('Filter displayed searches'))
        h.addWidget(ft)
        self.cft = cft = QToolButton(self)
        cft.setToolTip(_('Clear filter')), cft.setIcon(
            QIcon(I('clear_left.png')))
        cft.clicked.connect(ft.clear)
        h.addWidget(cft)
        l.addLayout(h)

        self.h2 = h = QHBoxLayout()
        self.searches = searches = QListView(self)
        searches.doubleClicked.connect(self.edit_search)
        self.model = SearchesModel(self.searches)
        self.model.dataChanged.connect(self.show_details)
        searches.setModel(self.model)
        searches.selectionModel().currentChanged.connect(self.show_details)
        searches.setSelectionMode(searches.ExtendedSelection)
        self.delegate = SearchDelegate(searches)
        searches.setItemDelegate(self.delegate)
        searches.setAlternatingRowColors(True)
        h.addWidget(searches, stretch=10)
        self.v = v = QVBoxLayout()
        h.addLayout(v)
        l.addLayout(h)

        def pb(text, tooltip=None):
            b = QPushButton(text, self)
            b.setToolTip(tooltip or '')
            b.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
            return b

        mulmsg = '\n\n' + _(
            'The entries are tried in order until the first one matches.')

        for text, action, tooltip in [
            (_('&Find'), 'find',
             _('Run the search using the selected entries.') + mulmsg),
            (_('&Replace'), 'replace',
             _('Run replace using the selected entries.') + mulmsg),
            (_('Replace a&nd Find'), 'replace-find',
             _('Run replace and then find using the selected entries.') +
             mulmsg),
            (_('Replace &all'), 'replace-all',
             _('Run Replace All for all selected entries in the order selected'
               )),
            (_('&Count all'), 'count',
             _('Run Count All for all selected entries')),
        ]:
            b = pb(text, tooltip)
            v.addWidget(b)
            b.clicked.connect(partial(self.run_search, action))

        self.d1 = d = QFrame(self)
        d.setFrameStyle(QFrame.HLine)
        v.addWidget(d)

        self.h3 = h = QHBoxLayout()
        self.upb = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-up.png'))), b.setToolTip(
            _('Move selected entries up'))
        b.clicked.connect(partial(self.move_entry, -1))
        self.dnb = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-down.png'))), b.setToolTip(
            _('Move selected entries down'))
        b.clicked.connect(partial(self.move_entry, 1))
        h.addWidget(self.upb), h.addWidget(self.dnb)
        v.addLayout(h)

        self.eb = b = pb(_('&Edit search'),
                         _('Edit the currently selected search'))
        b.clicked.connect(self.edit_search)
        v.addWidget(b)

        self.eb = b = pb(_('Re&move search'),
                         _('Remove the currently selected searches'))
        b.clicked.connect(self.remove_search)
        v.addWidget(b)

        self.eb = b = pb(_('&Add search'), _('Add a new saved search'))
        b.clicked.connect(self.add_search)
        v.addWidget(b)

        self.d2 = d = QFrame(self)
        d.setFrameStyle(QFrame.HLine)
        v.addWidget(d)

        self.where_box = wb = WhereBox(self, emphasize=True)
        self.where = SearchWidget.DEFAULT_STATE['where']
        v.addWidget(wb)
        self.direction_box = db = DirectionBox(self)
        self.direction = SearchWidget.DEFAULT_STATE['direction']
        v.addWidget(db)

        self.wr = wr = QCheckBox(_('&Wrap'))
        wr.setToolTip('<p>' + _(
            'When searching reaches the end, wrap around to the beginning and continue the search'
        ))
        self.wr.setChecked(SearchWidget.DEFAULT_STATE['wrap'])
        v.addWidget(wr)

        self.description = d = QLabel(' \n \n ')
        d.setTextFormat(Qt.PlainText)
        l.addWidget(d)

        l.addWidget(self.bb)
        self.bb.clear()
        self.bb.addButton(self.bb.Close)
        self.ib = b = self.bb.addButton(_('&Import'), self.bb.ActionRole)
        b.clicked.connect(self.import_searches)
        self.eb = b = self.bb.addButton(_('E&xport'), self.bb.ActionRole)
        self.em = m = QMenu(_('Export'))
        m.addAction(
            _('Export All'), lambda: QTimer.singleShot(
                0, partial(self.export_searches, all=True)))
        m.addAction(
            _('Export Selected'), lambda: QTimer.singleShot(
                0, partial(self.export_searches, all=False)))
        b.setMenu(m)

        self.searches.setFocus(Qt.OtherFocusReason)

    @dynamic_property
    def where(self):
        def fget(self):
            return self.where_box.where

        def fset(self, val):
            self.where_box.where = val

        return property(fget=fget, fset=fset)

    @dynamic_property
    def direction(self):
        def fget(self):
            return self.direction_box.direction

        def fset(self, val):
            self.direction_box.direction = val

        return property(fget=fget, fset=fset)

    @dynamic_property
    def wrap(self):
        def fget(self):
            return self.wr.isChecked()

        def fset(self, val):
            self.wr.setChecked(bool(val))

        return property(fget=fget, fset=fset)

    def do_filter(self, text):
        self.model.do_filter(text)
        self.searches.scrollTo(self.model.index(0))

    def run_search(self, action):
        searches, seen = [], set()
        for index in self.searches.selectionModel().selectedIndexes():
            if index.row() in seen:
                continue
            seen.add(index.row())
            search = SearchWidget.DEFAULT_STATE.copy()
            del search['mode']
            search_index, s = index.data(Qt.UserRole).toPyObject()
            search.update(s)
            search['wrap'] = self.wrap
            search['direction'] = self.direction
            search['where'] = self.where
            search['mode'] = search.get('mode', 'regex')
            searches.append(search)
        if not searches:
            return
        self.run_saved_searches.emit(searches, action)

    def move_entry(self, delta):
        rows = {
            index.row()
            for index in self.searches.selectionModel().selectedIndexes()
        } - {-1}
        if rows:
            with tprefs:
                for row in sorted(rows, reverse=delta > 0):
                    self.model.move_entry(row, delta)
            nrow = row + delta
            index = self.model.index(nrow)
            if index.isValid():
                sm = self.searches.selectionModel()
                sm.setCurrentIndex(index, sm.ClearAndSelect)

    def edit_search(self):
        index = self.searches.currentIndex()
        if index.isValid():
            search_index, search = index.data(Qt.UserRole).toPyObject()
            d = EditSearch(search=search,
                           search_index=search_index,
                           parent=self)
            if d.exec_() == d.Accepted:
                self.model.dataChanged.emit(index, index)

    def remove_search(self):
        rows = {
            index.row()
            for index in self.searches.selectionModel().selectedIndexes()
        } - {-1}
        self.model.remove_searches(rows)
        self.show_details()

    def add_search(self):
        d = EditSearch(parent=self)
        self._add_search(d)

    def _add_search(self, d):
        if d.exec_() == d.Accepted:
            self.model.add_searches()
            index = self.model.index(self.model.rowCount() - 1)
            self.searches.scrollTo(index)
            sm = self.searches.selectionModel()
            sm.setCurrentIndex(index, sm.ClearAndSelect)
            self.show_details()

    def add_predefined_search(self, state):
        d = EditSearch(parent=self, state=state)
        self._add_search(d)

    def show_details(self):
        self.description.setText(' \n \n ')
        i = self.searches.currentIndex()
        if i.isValid():
            search_index, search = i.data(Qt.UserRole).toPyObject()
            cs = '✓' if search.get(
                'case_sensitive',
                SearchWidget.DEFAULT_STATE['case_sensitive']) else '✗'
            da = '✓' if search.get(
                'dot_all', SearchWidget.DEFAULT_STATE['dot_all']) else '✗'
            if search.get('mode',
                          SearchWidget.DEFAULT_STATE['mode']) == 'regex':
                ts = _('(Case sensitive: {0} Dot All: {1})').format(cs, da)
            else:
                ts = _('(Case sensitive: {0} [Normal search])').format(cs)
            self.description.setText(
                _('{2} {3}\nFind: {0}\nReplace: {1}').format(
                    search.get('find', ''), search.get('replace', ''),
                    search.get('name', ''), ts))

    def import_searches(self):
        path = choose_files(self,
                            'import_saved_searches',
                            _('Choose file'),
                            filters=[(_('Saved Searches'), ['json'])],
                            all_files=False,
                            select_only_single_file=True)
        if path:
            with open(path[0], 'rb') as f:
                obj = json.loads(f.read())
            needed_keys = {
                'name', 'find', 'replace', 'case_sensitive', 'dot_all', 'mode'
            }

            def err():
                error_dialog(
                    self,
                    _('Invalid data'),
                    _('The file %s does not contain valid saved searches') %
                    path,
                    show=True)

            if not isinstance(
                    obj, dict
            ) or 'version' not in obj or 'searches' not in obj or obj[
                    'version'] not in (1, ):
                return err()
            searches = []
            for item in obj['searches']:
                if not isinstance(item, dict) or not set(
                        item.iterkeys()).issuperset(needed_keys):
                    return err
                searches.append({k: item[k] for k in needed_keys})

            if searches:
                tprefs['saved_searches'] = tprefs['saved_searches'] + searches
                count = len(searches)
                self.model.add_searches(count=count)
                sm = self.searches.selectionModel()
                top, bottom = self.model.index(self.model.rowCount() -
                                               count), self.model.index(
                                                   self.model.rowCount() - 1)
                sm.select(QItemSelection(top, bottom), sm.ClearAndSelect)
                self.searches.scrollTo(bottom)

    def export_searches(self, all=True):
        if all:
            searches = copy.deepcopy(tprefs['saved_searches'])
            if not searches:
                return error_dialog(self,
                                    _('No searches'),
                                    _('No searches available to be saved'),
                                    show=True)
        else:
            searches = []
            for index in self.searches.selectionModel().selectedIndexes():
                search = index.data(Qt.UserRole).toPyObject()[-1]
                searches.append(search.copy())
            if not searches:
                return error_dialog(self,
                                    _('No searches'),
                                    _('No searches selected'),
                                    show=True)
        [s.__setitem__('mode', s.get('mode', 'regex')) for s in searches]
        path = choose_save_file(self,
                                'export-saved-searches',
                                _('Choose file'),
                                filters=[(_('Saved Searches'), ['json'])],
                                all_files=False)
        if path:
            if not path.lower().endswith('.json'):
                path += '.json'
            raw = json.dumps({
                'version': 1,
                'searches': searches
            },
                             ensure_ascii=False,
                             indent=2,
                             sort_keys=True)
            with open(path, 'wb') as f:
                f.write(raw.encode('utf-8'))
Exemple #10
0
class SearchWidget(QWidget):

    DEFAULT_STATE = {
        'mode': 'normal',
        'where': 'current',
        'case_sensitive': False,
        'direction': 'down',
        'wrap': True,
        'dot_all': False,
    }

    search_triggered = pyqtSignal(object)
    save_search = pyqtSignal()
    show_saved_searches = pyqtSignal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QGridLayout(self)
        l.setContentsMargins(0, 0, 0, 0)
        self.setLayout(l)

        self.fl = fl = QLabel(_('&Find:'))
        fl.setAlignment(Qt.AlignRight | Qt.AlignCenter)
        self.find_text = ft = HistoryLineEdit(self, _('Clear search history'))
        ft.save_search.connect(self.save_search)
        ft.show_saved_searches.connect(self.show_saved_searches)
        ft.initialize('tweak_book_find_edit')
        ft.returnPressed.connect(lambda: self.search_triggered.emit('find'))
        fl.setBuddy(ft)
        l.addWidget(fl, 0, 0)
        l.addWidget(ft, 0, 1)

        self.rl = rl = QLabel(_('&Replace:'))
        rl.setAlignment(Qt.AlignRight | Qt.AlignCenter)
        self.replace_text = rt = HistoryLineEdit(self,
                                                 _('Clear replace history'))
        rt.save_search.connect(self.save_search)
        rt.show_saved_searches.connect(self.show_saved_searches)
        rt.initialize('tweak_book_replace_edit')
        rl.setBuddy(rt)
        l.addWidget(rl, 1, 0)
        l.addWidget(rt, 1, 1)
        l.setColumnStretch(1, 10)

        self.fb = fb = PushButton(_('&Find'), 'find', self)
        self.rfb = rfb = PushButton(_('Replace a&nd Find'), 'replace-find',
                                    self)
        self.rb = rb = PushButton(_('&Replace'), 'replace', self)
        self.rab = rab = PushButton(_('Replace &all'), 'replace-all', self)
        l.addWidget(fb, 0, 2)
        l.addWidget(rfb, 0, 3)
        l.addWidget(rb, 1, 2)
        l.addWidget(rab, 1, 3)

        self.ml = ml = QLabel(_('&Mode:'))
        self.ol = ol = QHBoxLayout()
        ml.setAlignment(Qt.AlignRight | Qt.AlignCenter)
        l.addWidget(ml, 2, 0)
        l.addLayout(ol, 2, 1, 1, 3)
        self.mode_box = mb = ModeBox(self)
        mb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        ml.setBuddy(mb)
        ol.addWidget(mb)

        self.where_box = wb = WhereBox(self)
        wb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        ol.addWidget(wb)

        self.direction_box = db = DirectionBox(self)
        db.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        ol.addWidget(db)

        self.cs = cs = QCheckBox(_('&Case sensitive'))
        cs.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        ol.addWidget(cs)

        self.wr = wr = QCheckBox(_('&Wrap'))
        wr.setToolTip('<p>' + _(
            'When searching reaches the end, wrap around to the beginning and continue the search'
        ))
        wr.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        ol.addWidget(wr)

        self.da = da = QCheckBox(_('&Dot all'))
        da.setToolTip('<p>' + _(
            "Make the '.' special character match any character at all, including a newline"
        ))
        da.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        ol.addWidget(da)

        self.mode_box.currentIndexChanged[int].connect(self.da.setVisible)

        ol.addStretch(10)

    @dynamic_property
    def mode(self):
        def fget(self):
            return self.mode_box.mode

        def fset(self, val):
            self.mode_box.mode = val
            self.da.setVisible(self.mode == 'regex')

        return property(fget=fget, fset=fset)

    @dynamic_property
    def find(self):
        def fget(self):
            return unicode(self.find_text.text())

        def fset(self, val):
            self.find_text.setText(val)

        return property(fget=fget, fset=fset)

    @dynamic_property
    def replace(self):
        def fget(self):
            return unicode(self.replace_text.text())

        def fset(self, val):
            self.replace_text.setText(val)

        return property(fget=fget, fset=fset)

    @dynamic_property
    def where(self):
        def fget(self):
            return self.where_box.where

        def fset(self, val):
            self.where_box.where = val

        return property(fget=fget, fset=fset)

    @dynamic_property
    def case_sensitive(self):
        def fget(self):
            return self.cs.isChecked()

        def fset(self, val):
            self.cs.setChecked(bool(val))

        return property(fget=fget, fset=fset)

    @dynamic_property
    def direction(self):
        def fget(self):
            return self.direction_box.direction

        def fset(self, val):
            self.direction_box.direction = val

        return property(fget=fget, fset=fset)

    @dynamic_property
    def wrap(self):
        def fget(self):
            return self.wr.isChecked()

        def fset(self, val):
            self.wr.setChecked(bool(val))

        return property(fget=fget, fset=fset)

    @dynamic_property
    def dot_all(self):
        def fget(self):
            return self.da.isChecked()

        def fset(self, val):
            self.da.setChecked(bool(val))

        return property(fget=fget, fset=fset)

    @dynamic_property
    def state(self):
        def fget(self):
            return {x: getattr(self, x) for x in self.DEFAULT_STATE}

        def fset(self, val):
            for x in self.DEFAULT_STATE:
                if x in val:
                    setattr(self, x, val[x])

        return property(fget=fget, fset=fset)

    def restore_state(self):
        self.state = tprefs.get('find-widget-state', self.DEFAULT_STATE)
        if self.where == 'selected-text':
            self.where = self.DEFAULT_STATE['where']

    def save_state(self):
        tprefs.set('find-widget-state', self.state)

    def pre_fill(self, text):
        if self.mode == 'regex':
            text = regex.escape(text, special_only=True)
        self.find = text
        self.find_text.setSelection(0, len(text) + 10)
Exemple #11
0
class Central(QStackedWidget):  # {{{

    ' The central widget, hosts the editors '

    current_editor_changed = pyqtSignal()
    close_requested = pyqtSignal(object)

    def __init__(self, parent=None):
        QStackedWidget.__init__(self, parent)
        self.welcome = w = QLabel('<p>'+_(
            'Double click a file in the left panel to start editing'
            ' it.'))
        self.addWidget(w)
        w.setWordWrap(True)
        w.setAlignment(Qt.AlignTop | Qt.AlignHCenter)

        self.container = c = QWidget(self)
        self.addWidget(c)
        l = c.l = QVBoxLayout(c)
        c.setLayout(l)
        l.setContentsMargins(0, 0, 0, 0)
        self.editor_tabs = t = QTabWidget(c)
        l.addWidget(t)
        t.setDocumentMode(True)
        t.setTabsClosable(True)
        t.setMovable(True)
        pal = self.palette()
        if pal.color(pal.WindowText).lightness() > 128:
            i = QImage(I('modified.png'))
            i.invertPixels()
            self.modified_icon = QIcon(QPixmap.fromImage(i))
        else:
            self.modified_icon = QIcon(I('modified.png'))
        self.editor_tabs.currentChanged.connect(self.current_editor_changed)
        self.editor_tabs.tabCloseRequested.connect(self._close_requested)
        self.search_panel = SearchPanel(self)
        l.addWidget(self.search_panel)
        self.restore_state()
        self.editor_tabs.tabBar().installEventFilter(self)

    def _close_requested(self, index):
        editor = self.editor_tabs.widget(index)
        self.close_requested.emit(editor)

    def add_editor(self, name, editor):
        fname = name.rpartition('/')[2]
        index = self.editor_tabs.addTab(editor, fname)
        self.editor_tabs.setTabToolTip(index, _('Full path:') + ' ' + name)
        editor.modification_state_changed.connect(self.editor_modified)

    def rename_editor(self, editor, name):
        for i in xrange(self.editor_tabs.count()):
            if self.editor_tabs.widget(i) is editor:
                fname = name.rpartition('/')[2]
                self.editor_tabs.setTabText(i, fname)
                self.editor_tabs.setTabToolTip(i, _('Full path:') + ' ' + name)

    def show_editor(self, editor):
        self.setCurrentIndex(1)
        self.editor_tabs.setCurrentWidget(editor)

    def close_editor(self, editor):
        for i in xrange(self.editor_tabs.count()):
            if self.editor_tabs.widget(i) is editor:
                self.editor_tabs.removeTab(i)
                if self.editor_tabs.count() == 0:
                    self.setCurrentIndex(0)
                return True
        return False

    def editor_modified(self, *args):
        tb = self.editor_tabs.tabBar()
        for i in xrange(self.editor_tabs.count()):
            editor = self.editor_tabs.widget(i)
            modified = getattr(editor, 'is_modified', False)
            tb.setTabIcon(i, self.modified_icon if modified else QIcon())

    def close_current_editor(self):
        ed = self.current_editor
        if ed is not None:
            self.close_requested.emit(ed)

    def close_all_but_current_editor(self):
        self.close_all_but(self.current_editor)

    def close_all_but(self, ed):
        close = []
        if ed is not None:
            for i in xrange(self.editor_tabs.count()):
                q = self.editor_tabs.widget(i)
                if q is not None and q is not ed:
                    close.append(q)
        for q in close:
            self.close_requested.emit(q)

    @property
    def current_editor(self):
        return self.editor_tabs.currentWidget()

    def save_state(self):
        tprefs.set('search-panel-visible', self.search_panel.isVisible())
        self.search_panel.save_state()

    def restore_state(self):
        self.search_panel.setVisible(tprefs.get('search-panel-visible', False))
        self.search_panel.restore_state()

    def show_find(self):
        self.search_panel.show_panel()

    def pre_fill_search(self, text):
        self.search_panel.pre_fill(text)

    def eventFilter(self, obj, event):
        base = super(Central, self)
        if obj is not self.editor_tabs.tabBar() or event.type() != QEvent.MouseButtonPress or event.button() not in (Qt.RightButton, Qt.MidButton):
            return base.eventFilter(obj, event)
        index = self.editor_tabs.tabBar().tabAt(event.pos())
        if index < 0:
            return base.eventFilter(obj, event)
        if event.button() == Qt.MidButton:
            self._close_requested(index)
        ed = self.editor_tabs.widget(index)
        if ed is not None:
            menu = QMenu(self)
            menu.addAction(actions['close-current-tab'].icon(), _('Close tab'), partial(self.close_requested.emit, ed))
            menu.addSeparator()
            menu.addAction(actions['close-all-but-current-tab'].icon(), _('Close other tabs'), partial(self.close_all_but, ed))
            menu.exec_(self.editor_tabs.tabBar().mapToGlobal(event.pos()))

        return True
Exemple #12
0
class MainWindowFrontend(QtGui.QMainWindow):
    printSignal = pyqtSignal(str)
    quitSignal = pyqtSignal()
    selectGxSignal = pyqtSignal(int)
    selectCidSignal = pyqtSignal(int)
    selectResSignal = pyqtSignal(int)
    selectNameSignal = pyqtSignal(str)
    changeCidSignal = pyqtSignal(int, str, str)
    changeGxSignal = pyqtSignal(int, str, bool)
    querySignal = pyqtSignal()

    def __init__(front, back):
        super(MainWindowFrontend, front).__init__()
        #print('[*front] creating frontend')
        front.prev_tbl_item = None
        front.ostream = None
        front.back = back
        front.ui = init_ui(front)
        # Progress bar is not hooked up yet
        #front.ui.progressBar.setVisible(False)
        front.connect_signals()
        front.steal_stdout()

    def steal_stdout(front):
        return _steal_stdout(front)

    def return_stdout(front):
        return _return_stdout(front)

    # TODO: this code is duplicated in back
    def user_info(front, *args, **kwargs):
        return guitools.user_info(front, *args, **kwargs)

    def user_input(front, *args, **kwargs):
        return guitools.user_input(front, *args, **kwargs)

    def user_option(front, *args, **kwargs):
        return guitools.user_option(front, *args, **kwargs)

    @slot_()
    def closeEvent(front, event):
        #front.printSignal.emit('[*front] closeEvent')
        event.accept()
        front.quitSignal.emit()

    def connect_signals(front):
        # Connect signals to slots
        back = front.back
        ui = front.ui
        # Frontend Signals
        front.printSignal.connect(back.backend_print)
        front.quitSignal.connect(back.quit)
        front.selectGxSignal.connect(back.select_gx)
        front.selectCidSignal.connect(back.select_cid)
        front.selectResSignal.connect(back.select_res_cid)
        front.selectNameSignal.connect(back.select_name)
        front.changeCidSignal.connect(back.change_chip_property)
        front.changeGxSignal.connect(back.change_image_property)
        front.querySignal.connect(back.query)

        # Menubar signals
        connect_file_signals(front)
        connect_action_signals(front)
        connect_option_signals(front)
        connect_batch_signals(front)
        connect_button_signals(front)  #-MD
        #connect_experimental_signals(front)
        connect_help_signals(front)
        #
        # Gui Components
        # Tables Widgets
        ui.cxs_TBL.itemClicked.connect(front.chip_tbl_clicked)
        ui.cxs_TBL.itemChanged.connect(front.chip_tbl_changed)
        ui.gxs_TBL.itemClicked.connect(front.img_tbl_clicked)
        ui.gxs_TBL.itemChanged.connect(front.img_tbl_changed)
        ui.res_TBL.itemClicked.connect(front.res_tbl_clicked)
        ui.res_TBL.itemChanged.connect(front.res_tbl_changed)
        ui.nxs_TBL.itemClicked.connect(front.name_tbl_clicked)
        # Tab Widget
        ui.tablesTabWidget.currentChanged.connect(front.change_view)
        ui.cxs_TBL.sortByColumn(0, Qt.AscendingOrder)
        ui.res_TBL.sortByColumn(0, Qt.AscendingOrder)
        ui.gxs_TBL.sortByColumn(0, Qt.AscendingOrder)

    def print(front, msg):
        print('[*front*] ' + msg)
        #front.printSignal.emit('[*front] ' + msg)

    @slot_(bool)
    def setEnabled(front, flag):
        #front.printDBG('setEnabled(%r)' % flag)
        ui = front.ui
        # Enable or disable all actions
        for uikey in ui.__dict__.keys():
            if uikey.find('action') == 0:
                ui.__dict__[uikey].setEnabled(flag)

        # The following options are always enabled
        ui.actionOpen_Database.setEnabled(True)
        ui.actionNew_Database.setEnabled(True)
        ui.actionQuit.setEnabled(True)
        ui.actionAbout.setEnabled(True)
        ui.actionView_Docs.setEnabled(True)
        ui.actionDelete_global_preferences.setEnabled(True)

        # The following options are no implemented. Disable them
        ui.actionConvert_all_images_into_chips.setEnabled(False)
        ui.actionBatch_Change_Name.setEnabled(False)
        ui.actionScale_all_ROIS.setEnabled(False)
        ui.actionWriteLogs.setEnabled(False)
        ui.actionAbout.setEnabled(False)
        ui.actionView_Docs.setEnabled(False)

    def _populate_table(front, tbl, col_headers, col_editable, row_list,
                        row2_datatup):
        #front.printDBG('_populate_table()')
        hheader = tbl.horizontalHeader()

        def set_header_context_menu(hheader):
            hheader.setContextMenuPolicy(Qt.CustomContextMenu)
            # TODO: for chip table: delete metedata column
            opt2_callback = [
                ('header', lambda: print('finishme')),
                ('cancel', lambda: print('cancel')),
            ]
            # HENDRIK / JASON TODO:
            # I have a small right-click context menu working
            # Maybe one of you can put some useful functions in these?
            popup_slot = guitools.popup_menu(tbl, opt2_callback)
            hheader.customContextMenuRequested.connect(popup_slot)

        def set_table_context_menu(tbl):
            tbl.setContextMenuPolicy(Qt.CustomContextMenu)
            # RCOS TODO: How do we get the clicked item on a right click?
            # tbl.selectedItems
            # tbl.selectedIndexes
            opt2_callback = [
                ('Query', front.querySignal.emit),
            ]
            #('item',  lambda: print('finishme')),
            #('cancel', lambda: print('cancel')), ]

            popup_slot = guitools.popup_menu(tbl, opt2_callback)
            tbl.customContextMenuRequested.connect(popup_slot)

        #set_header_context_menu(hheader)
        set_table_context_menu(tbl)

        sort_col = hheader.sortIndicatorSection()
        sort_ord = hheader.sortIndicatorOrder()
        tbl.sortByColumn(0, Qt.AscendingOrder)  # Basic Sorting
        tblWasBlocked = tbl.blockSignals(True)
        tbl.clear()
        tbl.setColumnCount(len(col_headers))
        tbl.setRowCount(len(row_list))
        tbl.verticalHeader().hide()
        tbl.setHorizontalHeaderLabels(col_headers)
        tbl.setSelectionMode(QAbstractItemView.SingleSelection)
        tbl.setSelectionBehavior(QAbstractItemView.SelectRows)
        tbl.setSortingEnabled(False)
        for row in iter(row_list):
            data_tup = row2_datatup[row]
            for col, data in enumerate(data_tup):
                item = QtGui.QTableWidgetItem()
                # RCOS TODO: Pass in datatype here.
                #if col_headers[col] == 'AIF':
                #print('col=%r dat=%r, %r' % (col, data, type(data)))
                if tools.is_bool(data) or data == 'True' or data == 'False':
                    bit = bool(data)
                    #print(bit)
                    if bit:
                        item.setCheckState(Qt.Checked)
                    else:
                        item.setCheckState(Qt.Unchecked)
                    #item.setData(Qt.DisplayRole, bool(data))
                elif tools.is_int(data):
                    item.setData(Qt.DisplayRole, int(data))
                elif tools.is_float(data):
                    item.setData(Qt.DisplayRole, float(data))
                else:
                    item.setText(str(data))

                item.setTextAlignment(Qt.AlignHCenter)
                if col_editable[col]:
                    item.setFlags(item.flags() | Qt.ItemIsEditable)
                    #print(item.getBackground())
                    item.setBackground(QtGui.QColor(250, 240, 240))
                else:
                    item.setFlags(item.flags() ^ Qt.ItemIsEditable)
                tbl.setItem(row, col, item)
        tbl.setSortingEnabled(True)
        tbl.sortByColumn(sort_col, sort_ord)  # Move back to old sorting
        tbl.show()
        tbl.blockSignals(tblWasBlocked)

    @slot_(str, list, list, list, list)
    @blocking
    def populate_tbl(front, table_name, col_headers, col_editable, row_list,
                     row2_datatup):
        table_name = str(table_name)
        #front.printDBG('populate_tbl(%s)' % table_name)
        try:
            tbl = front.ui.__dict__['%s_TBL' % table_name]
        except KeyError:
            valid_table_names = [
                key for key in front.ui.__dict__.keys()
                if key.find('_TBL') >= 0
            ]
            msg = '\n'.join([
                'Invalid table_name = %s_TBL' % table_name,
                'valid names:\n  ' + '\n  '.join(valid_table_names)
            ])
            raise Exception(msg)
        front._populate_table(tbl, col_headers, col_editable, row_list,
                              row2_datatup)

    def isItemEditable(self, item):
        return int(Qt.ItemIsEditable & item.flags()) == int(Qt.ItemIsEditable)

    #=======================
    # General Table Getters
    #=======================

    def get_tbl_header(front, tbl, col):
        # Map the fancy header back to the internal one.
        fancy_header = str(tbl.horizontalHeaderItem(col).text())
        header = (front.back.reverse_fancy[fancy_header] if fancy_header
                  in front.back.reverse_fancy else fancy_header)
        return header

    def get_tbl_int(front, tbl, row, col):
        return int(tbl.item(row, col).text())

    def get_tbl_str(front, tbl, row, col):
        return str(tbl.item(row, col).text())

    def get_header_val(front, tbl, header, row):
        # RCOS TODO: This is hacky. These just need to be
        # in dicts to begin with.
        tblname = str(tbl.objectName()).replace('_TBL', '')
        tblname = tblname.replace('image', 'img')  # Sooooo hack
        # TODO: backmap from fancy headers to consise
        col = front.back.table_headers[tblname].index(header)
        return tbl.item(row, col).text()

    #=======================
    # Specific Item Getters
    #=======================

    def get_chiptbl_header(front, col):
        return front.get_tbl_header(front.ui.cxs_TBL, col)

    def get_imgtbl_header(front, col):
        return front.get_tbl_header(front.ui.gxs_TBL, col)

    def get_restbl_header(front, col):
        return front.get_tbl_header(front.ui.res_TBL, col)

    def get_nametbl_header(front, col):
        return front.get_tbl_header(front.ui.nxs_TBL, col)

    def get_restbl_cid(front, row):
        return int(front.get_header_val(front.ui.res_TBL, 'cid', row))

    def get_chiptbl_cid(front, row):
        return int(front.get_header_val(front.ui.cxs_TBL, 'cid', row))

    def get_nametbl_name(front, row):
        return str(front.get_header_val(front.ui.nxs_TBL, 'name', row))

    def get_imgtbl_gx(front, row):
        return int(front.get_header_val(front.ui.gxs_TBL, 'gx', row))

    #=======================
    # Table Changed Functions
    #=======================

    @slot_(QtGui.QTableWidgetItem)
    def img_tbl_changed(front, item):
        front.print('img_tbl_changed()')
        row, col = (item.row(), item.column())
        sel_gx = front.get_imgtbl_gx(row)
        header_lbl = front.get_imgtbl_header(col)
        new_val = item.checkState() == Qt.Checked
        front.changeGxSignal.emit(sel_gx, header_lbl, new_val)

    @slot_(QtGui.QTableWidgetItem)
    def chip_tbl_changed(front, item):
        front.print('chip_tbl_changed()')
        row, col = (item.row(), item.column())
        sel_cid = front.get_chiptbl_cid(row)  # Get selected chipid
        new_val = csv_sanatize(item.text())  # sanatize for csv
        header_lbl = front.get_chiptbl_header(col)  # Get changed column
        front.changeCidSignal.emit(sel_cid, header_lbl, new_val)

    @slot_(QtGui.QTableWidgetItem)
    def res_tbl_changed(front, item):
        front.print('res_tbl_changed()')
        row, col = (item.row(), item.column())
        sel_cid = front.get_restbl_cid(row)  # The changed row's chip id
        new_val = csv_sanatize(item.text())  # sanatize val for csv
        header_lbl = front.get_restbl_header(col)  # Get changed column
        front.changeCidSignal.emit(sel_cid, header_lbl, new_val)

    #=======================
    # Table Clicked Functions
    #=======================
    @slot_(QtGui.QTableWidgetItem)
    @clicked
    def img_tbl_clicked(front, item):
        row = item.row()
        front.print('img_tbl_clicked(%r)' % (row))
        sel_gx = front.get_imgtbl_gx(row)
        front.selectGxSignal.emit(sel_gx)

    @slot_(QtGui.QTableWidgetItem)
    @clicked
    def chip_tbl_clicked(front, item):
        row, col = (item.row(), item.column())
        front.print('chip_tbl_clicked(%r, %r)' % (row, col))
        sel_cid = front.get_chiptbl_cid(row)
        front.selectCidSignal.emit(sel_cid)

    @slot_(QtGui.QTableWidgetItem)
    @clicked
    def res_tbl_clicked(front, item):
        row, col = (item.row(), item.column())
        front.print('res_tbl_clicked(%r, %r)' % (row, col))
        sel_cid = front.get_restbl_cid(row)
        front.selectResSignal.emit(sel_cid)

    @slot_(QtGui.QTableWidgetItem)
    @clicked
    def name_tbl_clicked(front, item):
        row, col = (item.row(), item.column())
        front.print('name_tbl_clicked(%r, %r)' % (row, col))
        sel_name = front.get_nametbl_name(row)
        front.selectNameSignal.emit(sel_name)

    #=======================
    # Other
    #=======================

    @slot_(int)
    def change_view(front, new_state):
        front.print('change_view()')
        prevBlock = front.ui.tablesTabWidget.blockSignals(True)
        front.ui.tablesTabWidget.blockSignals(prevBlock)

    @slot_(str, str, list)
    def modal_useroption(front, msg, title, options):
        pass

    @slot_(str)
    def gui_write(front, msg_):
        app = front.back.app
        outputEdit = front.ui.outputEdit
        # Write msg to text area
        outputEdit.moveCursor(QtGui.QTextCursor.End)
        # TODO: Find out how to do backspaces in textEdit
        msg = str(msg_)
        if msg.find('\b') != -1:
            msg = msg.replace('\b', '') + '\n'
        outputEdit.insertPlainText(msg)
        if app is not None:
            app.processEvents()

    @slot_()
    def gui_flush(front):
        app = front.back.app
        if app is not None:
            app.processEvents()
Exemple #13
0
class Preferences(QMainWindow):

    run_wizard_requested = pyqtSignal()

    def __init__(self, gui, initial_plugin=None, close_after_initial=False):
        QMainWindow.__init__(self, gui)
        self.gui = gui
        self.must_restart = False
        self.committed = False
        self.close_after_initial = close_after_initial

        self.resize(930, 720)
        nh, nw = min_available_height()-25, available_width()-10
        if nh < 0:
            nh = 800
        if nw < 0:
            nw = 600
        nh = min(self.height(), nh)
        nw = min(self.width(), nw)
        self.resize(nw, nh)
        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
        self.esc_action.triggered.connect(self.esc)

        geom = gprefs.get('preferences_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)

        # Center
        if islinux:
            self.move(gui.rect().center() - self.rect().center())

        self.setWindowModality(Qt.WindowModal)
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
        self.setWindowIcon(QIcon(I('config.png')))

        self.status_bar = StatusBar(self)
        self.setStatusBar(self.status_bar)

        self.stack = QStackedWidget(self)
        self.cw = QWidget(self)
        self.cw.setLayout(QVBoxLayout())
        self.cw.layout().addWidget(self.stack)
        self.bb = QDialogButtonBox(QDialogButtonBox.Close)
        self.wizard_button = self.bb.addButton(_('Run welcome wizard'),
                self.bb.ActionRole)
        self.wizard_button.setIcon(QIcon(I('wizard.png')))
        self.wizard_button.clicked.connect(self.run_wizard,
                type=Qt.QueuedConnection)
        self.cw.layout().addWidget(self.bb)
        self.bb.button(self.bb.Close).setDefault(True)
        self.bb.rejected.connect(self.close, type=Qt.QueuedConnection)
        self.setCentralWidget(self.cw)
        self.browser = Browser(self)
        self.browser.show_plugin.connect(self.show_plugin)
        self.stack.addWidget(self.browser)
        self.scroll_area = QScrollArea(self)
        self.stack.addWidget(self.scroll_area)
        self.scroll_area.setWidgetResizable(True)

        self.bar = QToolBar(self)
        self.addToolBar(self.bar)
        self.bar.setVisible(False)
        self.bar.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
        self.bar.setMovable(False)
        self.bar.setFloatable(False)
        self.bar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.apply_action = self.bar.addAction(QIcon(I('ok.png')), _('&Apply'),
                self.commit)
        self.cancel_action = self.bar.addAction(QIcon(I('window-close.png')),
                _('&Cancel'),                self.cancel)
        self.bar_title = BarTitle(self.bar)
        self.bar.addWidget(self.bar_title)
        self.restore_action = self.bar.addAction(QIcon(I('clear_left.png')),
                _('Restore &defaults'), self.restore_defaults)
        for ac, tt in [('apply', _('Save changes')),
                ('cancel', _('Cancel and return to overview'))]:
            ac = getattr(self, ac+'_action')
            ac.setToolTip(tt)
            ac.setWhatsThis(tt)
            ac.setStatusTip(tt)

        for ch in self.bar.children():
            if isinstance(ch, QToolButton):
                ch.setCursor(Qt.PointingHandCursor)
                ch.setAutoRaise(True)

        self.stack.setCurrentIndex(0)

        if initial_plugin is not None:
            category, name = initial_plugin
            plugin = get_plugin(category, name)
            if plugin is not None:
                self.show_plugin(plugin)

    def run_wizard(self):
        self.close()
        self.run_wizard_requested.emit()

    def set_tooltips_for_labels(self):

        def process_child(child):
            for g in child.children():
                if isinstance(g, QLabel):
                    buddy = g.buddy()
                    if buddy is not None and hasattr(buddy, 'toolTip'):
                        htext = unicode(buddy.toolTip()).strip()
                        etext = unicode(g.toolTip()).strip()
                        if htext and not etext:
                            g.setToolTip(htext)
                            g.setWhatsThis(htext)
                else:
                    process_child(g)

        process_child(self.showing_widget)

    def show_plugin(self, plugin):
        self.showing_widget = plugin.create_widget(self.scroll_area)
        self.showing_widget.genesis(self.gui)
        self.showing_widget.initialize()
        self.set_tooltips_for_labels()
        self.scroll_area.setWidget(self.showing_widget)
        self.stack.setCurrentIndex(1)
        self.showing_widget.show()
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences') + ' - ' +
                plugin.gui_name)
        self.apply_action.setEnabled(False)
        self.showing_widget.changed_signal.connect(lambda :
                self.apply_action.setEnabled(True))
        self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults)
        tt = self.showing_widget.restore_defaults_desc
        if not self.restore_action.isEnabled():
            tt = _('Restoring to defaults not supported for') + ' ' + \
                plugin.gui_name
        self.restore_action.setToolTip(textwrap.fill(tt))
        self.restore_action.setWhatsThis(textwrap.fill(tt))
        self.restore_action.setStatusTip(tt)
        self.bar_title.show_plugin(plugin)
        self.setWindowIcon(QIcon(plugin.icon))
        self.bar.setVisible(True)
        self.bb.setVisible(False)


    def hide_plugin(self):
        self.showing_widget = QWidget(self.scroll_area)
        self.scroll_area.setWidget(self.showing_widget)
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
        self.bar.setVisible(False)
        self.stack.setCurrentIndex(0)
        self.setWindowIcon(QIcon(I('config.png')))
        self.bb.setVisible(True)

    def esc(self, *args):
        if self.stack.currentIndex() == 1:
            self.cancel()
        elif self.stack.currentIndex() == 0:
            self.close()

    def commit(self, *args):
        try:
            must_restart = self.showing_widget.commit()
        except AbortCommit:
            return
        rc = self.showing_widget.restart_critical
        self.committed = True
        do_restart = False
        if must_restart:
            self.must_restart = True
            msg = _('Some of the changes you made require a restart.'
                    ' Please restart calibre as soon as possible.')
            if rc:
                msg = _('The changes you have made require calibre be '
                        'restarted immediately. You will not be allowed to '
                        'set any more preferences, until you restart.')


            d = warning_dialog(self, _('Restart needed'), msg,
                    show_copy_button=False)
            b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
            b.setIcon(QIcon(I('lt.png')))
            d.do_restart = False
            def rf():
                d.do_restart = True
            b.clicked.connect(rf)
            d.set_details('')
            d.exec_()
            b.clicked.disconnect()
            do_restart = d.do_restart
        self.showing_widget.refresh_gui(self.gui)
        self.hide_plugin()
        if self.close_after_initial or (must_restart and rc) or do_restart:
            self.close()
        if do_restart:
            self.gui.quit(restart=True)


    def cancel(self, *args):
        if self.close_after_initial:
            self.close()
        else:
            self.hide_plugin()

    def restore_defaults(self, *args):
        self.showing_widget.restore_defaults()

    def closeEvent(self, *args):
        gprefs.set('preferences_window_geometry',
                bytearray(self.saveGeometry()))
        if self.committed:
            self.gui.must_restart_before_config = self.must_restart
            self.gui.tags_view.recount()
            self.gui.create_device_menu()
            self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
            self.gui.bars_manager.apply_settings()
            self.gui.bars_manager.update_bars()
            self.gui.build_context_menus()

        return QMainWindow.closeEvent(self, *args)
Exemple #14
0
class Splitter(QSplitter):

    state_changed = pyqtSignal(object)

    def __init__(self, name, label, icon, initial_show=True,
            initial_side_size=120, connect_button=True,
            orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None):
        QSplitter.__init__(self, parent)
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.desired_side_size = initial_side_size
        self.desired_show = initial_show
        self.resize_timer.setInterval(5)
        self.resize_timer.timeout.connect(self.do_resize)
        self.setOrientation(orientation)
        self.side_index = side_index
        self._name = name
        self.label = label
        self.initial_side_size = initial_side_size
        self.initial_show = initial_show
        self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection)
        self.button = LayoutButton(icon, label, self, shortcut=shortcut)
        if connect_button:
            self.button.clicked.connect(self.double_clicked)

        if shortcut is not None:
            self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
                    self)
            self.action_toggle.triggered.connect(self.toggle_triggered)
            if parent is not None:
                parent.addAction(self.action_toggle)
                if hasattr(parent, 'keyboard'):
                    parent.keyboard.register_shortcut('splitter %s %s'%(name,
                        label), unicode(self.action_toggle.text()),
                        default_keys=(shortcut,), action=self.action_toggle)
                else:
                    self.action_toggle.setShortcut(shortcut)
            else:
                self.action_toggle.setShortcut(shortcut)

    def toggle_triggered(self, *args):
        self.toggle_side_pane()

    def createHandle(self):
        return SplitterHandle(self.orientation(), self)

    def initialize(self):
        for i in range(self.count()):
            h = self.handle(i)
            if h is not None:
                h.splitter_moved()
        self.state_changed.emit(not self.is_side_index_hidden)

    def splitter_moved(self, *args):
        self.desired_side_size = self.side_index_size
        self.state_changed.emit(not self.is_side_index_hidden)

    @property
    def is_side_index_hidden(self):
        sizes = list(self.sizes())
        try:
            return sizes[self.side_index] == 0
        except IndexError:
            return True

    @property
    def save_name(self):
        ori = 'horizontal' if self.orientation() == Qt.Horizontal \
                else 'vertical'
        return self._name + '_' + ori

    def print_sizes(self):
        if self.count() > 1:
            print self.save_name, 'side:', self.side_index_size, 'other:',
            print list(self.sizes())[self.other_index]

    @dynamic_property
    def side_index_size(self):
        def fget(self):
            if self.count() < 2:
                return 0
            return self.sizes()[self.side_index]

        def fset(self, val):
            if self.count() < 2:
                return
            if val == 0 and not self.is_side_index_hidden:
                self.save_state()
            sizes = list(self.sizes())
            for i in range(len(sizes)):
                sizes[i] = val if i == self.side_index else 10
            self.setSizes(sizes)
            total = sum(self.sizes())
            sizes = list(self.sizes())
            for i in range(len(sizes)):
                sizes[i] = val if i == self.side_index else total-val
            self.setSizes(sizes)
            self.initialize()

        return property(fget=fget, fset=fset)

    def do_resize(self, *args):
        orig = self.desired_side_size
        QSplitter.resizeEvent(self, self._resize_ev)
        if orig > 20 and self.desired_show:
            c = 0
            while abs(self.side_index_size - orig) > 10 and c < 5:
                self.apply_state(self.get_state(), save_desired=False)
                c += 1

    def resizeEvent(self, ev):
        if self.resize_timer.isActive():
            self.resize_timer.stop()
        self._resize_ev = ev
        self.resize_timer.start()

    def get_state(self):
        if self.count() < 2:
            return (False, 200)
        return (self.desired_show, self.desired_side_size)

    def apply_state(self, state, save_desired=True):
        if state[0]:
            self.side_index_size = state[1]
            if save_desired:
                self.desired_side_size = self.side_index_size
        else:
            self.side_index_size = 0
        self.desired_show = state[0]

    def default_state(self):
        return (self.initial_show, self.initial_side_size)

    # Public API {{{

    def update_desired_state(self):
        self.desired_show = not self.is_side_index_hidden

    def save_state(self):
        if self.count() > 1:
            gprefs[self.save_name+'_state'] = self.get_state()

    @property
    def other_index(self):
        return (self.side_index+1)%2

    def restore_state(self):
        if self.count() > 1:
            state = gprefs.get(self.save_name+'_state',
                    self.default_state())
            self.apply_state(state, save_desired=False)
            self.desired_side_size = state[1]

    def toggle_side_pane(self, hide=None):
        if hide is None:
            action = 'show' if self.is_side_index_hidden else 'hide'
        else:
            action = 'hide' if hide else 'show'
        getattr(self, action+'_side_pane')()

    def show_side_pane(self):
        if self.count() < 2 or not self.is_side_index_hidden:
            return
        if self.desired_side_size == 0:
            self.desired_side_size = self.initial_side_size
        self.apply_state((True, self.desired_side_size))

    def hide_side_pane(self):
        if self.count() < 2 or self.is_side_index_hidden:
            return
        self.apply_state((False, self.desired_side_size))

    def double_clicked(self, *args):
        self.toggle_side_pane()
Exemple #15
0
class FilenamePattern(QWidget, Ui_Form):  # {{{

    changed_signal = pyqtSignal()

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

        self.test_button.clicked[()].connect(self.do_test)
        self.re.lineEdit().returnPressed[()].connect(self.do_test)
        self.filename.returnPressed[()].connect(self.do_test)
        self.re.lineEdit().textChanged.connect(lambda x: self.changed_signal.emit())

    def initialize(self, defaults=False):
        # Get all itmes in the combobox. If we are resting
        # to defaults we don't want to lose what the user
        # has added.
        val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
        self.re.clear()

        if defaults:
            val = prefs.defaults['filename_pattern']
        else:
            val = prefs['filename_pattern']
        self.re.lineEdit().setText(val)

        val_hist += gprefs.get('filename_pattern_history', [
                               '(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
        if val in val_hist:
            del val_hist[val_hist.index(val)]
        val_hist.insert(0, val)
        for v in val_hist:
            # Ensure we don't have duplicate items.
            if v and self.re.findText(v) == -1:
                self.re.addItem(v)
        self.re.setCurrentIndex(0)

    def do_test(self):
        from calibre.ebooks.metadata.meta import metadata_from_filename
        fname = unicode(self.filename.text())
        ext = os.path.splitext(fname)[1][1:].lower()
        if ext not in BOOK_EXTENSIONS:
            return warning_dialog(self, _('Test name invalid'),
                    _('The name <b>%r</b> does not appear to end with a'
                        ' file extension. The name must end with a file '
                        ' extension like .epub or .mobi')%fname, show=True)

        try:
            pat = self.pattern()
        except Exception as err:
            error_dialog(self, _('Invalid regular expression'),
                         _('Invalid regular expression: %s')%err).exec_()
            return
        mi = metadata_from_filename(fname, pat)
        if mi.title:
            self.title.setText(mi.title)
        else:
            self.title.setText(_('No match'))
        if mi.authors:
            self.authors.setText(', '.join(mi.authors))
        else:
            self.authors.setText(_('No match'))

        if mi.series:
            self.series.setText(mi.series)
        else:
            self.series.setText(_('No match'))

        if mi.series_index is not None:
            self.series_index.setText(str(mi.series_index))
        else:
            self.series_index.setText(_('No match'))

        if mi.publisher:
            self.publisher.setText(mi.publisher)

        if mi.pubdate:
            self.pubdate.setText(mi.pubdate.strftime('%Y-%m-%d'))

        self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))

    def pattern(self):
        pat = unicode(self.re.lineEdit().text())
        return re.compile(pat)

    def commit(self):
        pat = self.pattern().pattern
        prefs['filename_pattern'] = pat

        history = []
        history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
        for p in history_pats[:14]:
            # Ensure we don't have duplicate items.
            if p and p not in history:
                history.append(p)
        gprefs['filename_pattern_history'] = history

        return pat
Exemple #16
0
class TextBrowser(PlainTextEdit):  # {{{

    resized = pyqtSignal()
    wheel_event = pyqtSignal(object)
    next_change = pyqtSignal(object)
    scrolled = pyqtSignal()
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, right=False, parent=None, show_open_in_editor=False):
        PlainTextEdit.__init__(self, parent)
        self.setFrameStyle(0)
        self.show_open_in_editor = show_open_in_editor
        self.side_margin = 0
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.setFocusPolicy(Qt.NoFocus)
        self.right = right
        self.setReadOnly(True)
        w = self.fontMetrics()
        self.number_width = max(map(lambda x: w.width(str(x)), xrange(10)))
        self.space_width = w.width(' ')
        self.setLineWrapMode(self.WidgetWidth)
        self.setTabStopWidth(tprefs['editor_tab_stop_width'] *
                             self.space_width)
        font = self.font()
        ff = tprefs['editor_font_family']
        if ff is None:
            ff = default_font_family()
        font.setFamily(ff)
        font.setPointSize(tprefs['editor_font_size'])
        self.setFont(font)
        font = self.heading_font = QFont(self.font())
        font.setPointSize(int(tprefs['editor_font_size'] * 1.5))
        font.setBold(True)
        theme = get_theme(tprefs['editor_theme'])
        pal = self.palette()
        pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg'))
        pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg'))
        pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg'))
        pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg'))
        pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg'))
        self.setPalette(pal)
        self.viewport().setCursor(Qt.ArrowCursor)
        self.line_number_area = LineNumbers(self)
        self.blockCountChanged[int].connect(self.update_line_number_area_width)
        self.updateRequest.connect(self.update_line_number_area)
        self.line_number_palette = pal = QPalette()
        pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg'))
        pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg'))
        pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg'))
        self.line_number_map = LineNumberMap()
        self.search_header_pos = 0
        self.changes, self.headers, self.images = [], [], OrderedDict()
        self.setVerticalScrollBarPolicy(
            Qt.ScrollBarAlwaysOff), self.setHorizontalScrollBarPolicy(
                Qt.ScrollBarAlwaysOff)
        self.diff_backgrounds = {
            'replace':
            theme_color(theme, 'DiffReplace', 'bg'),
            'insert':
            theme_color(theme, 'DiffInsert', 'bg'),
            'delete':
            theme_color(theme, 'DiffDelete', 'bg'),
            'replacereplace':
            theme_color(theme, 'DiffReplaceReplace', 'bg'),
            'boundary':
            QBrush(theme_color(theme, 'Normal', 'fg'), Qt.Dense7Pattern),
        }
        self.diff_foregrounds = {
            'replace': theme_color(theme, 'DiffReplace', 'fg'),
            'insert': theme_color(theme, 'DiffInsert', 'fg'),
            'delete': theme_color(theme, 'DiffDelete', 'fg'),
            'boundary': QColor(0, 0, 0, 0),
        }
        for x in ('replacereplace', 'insert', 'delete'):
            f = QTextCharFormat()
            f.setBackground(self.diff_backgrounds[x])
            setattr(self, '%s_format' % x, f)

    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        i = unicode(self.textCursor().selectedText()).rstrip('\0')
        if i:
            a(QIcon(I('edit-copy.png')), _('Copy to clipboard'),
              self.copy).setShortcut(QKeySequence.Copy)

        if len(self.changes) > 0:
            a(QIcon(I('arrow-up.png')), _('Previous change'),
              partial(self.next_change.emit, -1))
            a(QIcon(I('arrow-down.png')), _('Next change'),
              partial(self.next_change.emit, 1))

        if self.show_open_in_editor:
            b = self.cursorForPosition(pos).block()
            if b.isValid():
                a(QIcon(I('tweak.png')), _('Open file in the editor'),
                  partial(self.generate_sync_request, b.blockNumber()))

        if len(m.actions()) > 0:
            m.exec_(self.mapToGlobal(pos))

    def mouseDoubleClickEvent(self, ev):
        if ev.button() == 1:
            b = self.cursorForPosition(ev.pos()).block()
            if b.isValid():
                self.generate_sync_request(b.blockNumber())
        return PlainTextEdit.mouseDoubleClickEvent(self, ev)

    def generate_sync_request(self, block_number):
        if not self.headers:
            return
        try:
            lnum = int(self.line_number_map.get(block_number, ''))
        except:
            lnum = 1
        for i, (num, text) in enumerate(self.headers):
            if num > block_number:
                name = text if i == 0 else self.headers[i - 1][1]
                break
        else:
            name = self.headers[-1][1]
        self.line_activated.emit(name, lnum, bool(self.right))

    def search(self, query, reverse=False):
        ''' Search for query, also searching the headers. Matches in headers
        are not highlighted as managing the highlight is too much of a pain.'''
        if not query.strip():
            return
        c = self.textCursor()
        lnum = c.block().blockNumber()
        cpos = c.positionInBlock()
        headers = dict(self.headers)
        if lnum in headers:
            cpos = self.search_header_pos
        lines = unicode(self.toPlainText()).splitlines()
        for hn, text in self.headers:
            lines[hn] = text
        prefix, postfix = lines[lnum][:cpos], lines[lnum][cpos:]
        before, after = enumerate(
            lines[0:lnum]), ((lnum + 1 + i, x)
                             for i, x in enumerate(lines[lnum + 1:]))
        if reverse:
            sl = chain([(lnum, prefix)], reversed(tuple(before)),
                       reversed(tuple(after)), [(lnum, postfix)])
        else:
            sl = chain([(lnum, postfix)], after, before, [(lnum, prefix)])
        flags = regex.REVERSE if reverse else 0
        pat = regex.compile(regex.escape(query, special_only=True),
                            flags=regex.UNICODE | regex.IGNORECASE | flags)
        for num, text in sl:
            try:
                m = next(pat.finditer(text))
            except StopIteration:
                continue
            start, end = m.span()
            length = end - start
            if text is postfix:
                start += cpos
            c = QTextCursor(self.document().findBlockByNumber(num))
            c.setPosition(c.position() + start)
            if num in headers:
                self.search_header_pos = start + length
            else:
                c.setPosition(c.position() + length, c.KeepAnchor)
                self.search_header_pos = 0
            if reverse:
                pos, anchor = c.position(), c.anchor()
                c.setPosition(pos), c.setPosition(anchor, c.KeepAnchor)
            self.setTextCursor(c)
            self.centerCursor()
            self.scrolled.emit()
            break
        else:
            info_dialog(self,
                        _('No matches found'),
                        _('No matches found for query: %s' % query),
                        show=True)

    def clear(self):
        PlainTextEdit.clear(self)
        self.line_number_map.clear()
        del self.changes[:]
        del self.headers[:]
        self.images.clear()
        self.search_header_pos = 0
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

    def update_line_number_area_width(self, block_count=0):
        self.side_margin = self.line_number_area_width()
        if self.right:
            self.setViewportMargins(0, 0, self.side_margin, 0)
        else:
            self.setViewportMargins(self.side_margin, 0, 0, 0)

    def available_width(self):
        return self.width() - self.side_margin

    def line_number_area_width(self):
        return 9 + (self.line_number_map.max_width * self.number_width)

    def update_line_number_area(self, rect, dy):
        if dy:
            self.line_number_area.scroll(0, dy)
        else:
            self.line_number_area.update(0, rect.y(),
                                         self.line_number_area.width(),
                                         rect.height())
        if rect.contains(self.viewport().rect()):
            self.update_line_number_area_width()

    def resizeEvent(self, ev):
        PlainTextEdit.resizeEvent(self, ev)
        cr = self.contentsRect()
        if self.right:
            self.line_number_area.setGeometry(
                QRect(cr.right() - self.line_number_area_width(), cr.top(),
                      cr.right(), cr.height()))
        else:
            self.line_number_area.setGeometry(
                QRect(cr.left(), cr.top(), self.line_number_area_width(),
                      cr.height()))
        self.resized.emit()

    def paint_line_numbers(self, ev):
        painter = QPainter(self.line_number_area)
        painter.fillRect(ev.rect(),
                         self.line_number_palette.color(QPalette.Base))

        block = self.firstVisibleBlock()
        num = block.blockNumber()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        painter.setPen(self.line_number_palette.color(QPalette.Text))
        change_starts = {x[0] for x in self.changes}

        while block.isValid() and top <= ev.rect().bottom():
            r = ev.rect()
            if block.isVisible() and bottom >= r.top():
                text = unicode(self.line_number_map.get(num, ''))
                is_start = text != '-' and num in change_starts
                if is_start:
                    painter.save()
                    f = QFont(self.font())
                    f.setBold(True)
                    painter.setFont(f)
                    painter.setPen(
                        self.line_number_palette.color(QPalette.BrightText))
                if text == '-':
                    painter.drawLine(r.left() + 2, (top + bottom) // 2,
                                     r.right() - 2, (top + bottom) // 2)
                else:
                    if self.right:
                        painter.drawText(r.left() + 3, top, r.right(),
                                         self.fontMetrics().height(),
                                         Qt.AlignLeft, text)
                    else:
                        painter.drawText(r.left() + 2, top,
                                         r.right() - 5,
                                         self.fontMetrics().height(),
                                         Qt.AlignRight, text)
                if is_start:
                    painter.restore()
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            num += 1

    def paintEvent(self, event):
        w = self.viewport().rect().width()
        painter = QPainter(self.viewport())
        painter.setClipRect(event.rect())
        floor = event.rect().bottom()
        ceiling = event.rect().top()
        fv = self.firstVisibleBlock().blockNumber()
        origin = self.contentOffset()
        doc = self.document()
        lines = []

        for num, text in self.headers:
            top, bot = num, num + 3
            if bot < fv:
                continue
            y_top = self.blockBoundingGeometry(
                doc.findBlockByNumber(top)).translated(origin).y()
            y_bot = self.blockBoundingGeometry(
                doc.findBlockByNumber(bot)).translated(origin).y()
            if max(y_top, y_bot) < ceiling:
                continue
            if min(y_top, y_bot) > floor:
                break
            painter.setFont(self.heading_font)
            br = painter.drawText(3, y_top, w, y_bot - y_top - 5,
                                  Qt.TextSingleLine, text)
            painter.setPen(QPen(self.palette().text(), 2))
            painter.drawLine(0, br.bottom() + 3, w, br.bottom() + 3)

        for top, bot, kind in self.changes:
            if bot < fv:
                continue
            y_top = self.blockBoundingGeometry(
                doc.findBlockByNumber(top)).translated(origin).y()
            y_bot = self.blockBoundingGeometry(
                doc.findBlockByNumber(bot)).translated(origin).y()
            if max(y_top, y_bot) < ceiling:
                continue
            if min(y_top, y_bot) > floor:
                break
            if y_top != y_bot:
                painter.fillRect(0, y_top, w, y_bot - y_top,
                                 self.diff_backgrounds[kind])
            lines.append((y_top, y_bot, kind))
            if top in self.images:
                img, maxw = self.images[top][:2]
                if bot > top + 1 and not img.isNull():
                    y_top = self.blockBoundingGeometry(
                        doc.findBlockByNumber(top +
                                              1)).translated(origin).y() + 3
                    y_bot -= 3
                    scaled, imgw, imgh = fit_image(img.width(), img.height(),
                                                   w - 3, y_bot - y_top)
                    painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
                    painter.drawPixmap(QRect(3, y_top, imgw, imgh), img)

        painter.end()
        PlainTextEdit.paintEvent(self, event)
        painter = QPainter(self.viewport())
        painter.setClipRect(event.rect())
        for top, bottom, kind in sorted(lines,
                                        key=lambda
                                        (t, b, k): {'replace': 0}.get(k, 1)):
            painter.setPen(QPen(self.diff_foregrounds[kind], 1))
            painter.drawLine(0, top, w, top)
            painter.drawLine(0, bottom - 1, w, bottom - 1)

    def wheelEvent(self, ev):
        if ev.orientation() == Qt.Vertical:
            self.wheel_event.emit(ev)
        else:
            return PlainTextEdit.wheelEvent(self, ev)
Exemple #17
0
class BasicSettings(QWidget):  # {{{

    changed_signal = pyqtSignal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.settings = {}
        self._prevent_changed = False
        self.Setting = namedtuple(
            'Setting', 'name prefs widget getter setter initial_value')

    def __call__(self,
                 name,
                 widget=None,
                 getter=None,
                 setter=None,
                 prefs=None):
        prefs = prefs or tprefs
        defval = prefs.defaults[name]
        inval = prefs[name]
        if widget is None:
            if isinstance(defval, bool):
                widget = QCheckBox(self)
                getter = getter or methodcaller('isChecked')
                setter = setter or (lambda x, v: x.setChecked(v))
                widget.toggled.connect(self.emit_changed)
            elif isinstance(defval, (int, float)):
                widget = (QSpinBox
                          if isinstance(defval, int) else QDoubleSpinBox)(self)
                getter = getter or methodcaller('value')
                setter = setter or (lambda x, v: x.setValue(v))
                widget.valueChanged.connect(self.emit_changed)
            else:
                raise TypeError('Unknown setting type for setting: %s' % name)
        else:
            if getter is None or setter is None:
                raise ValueError("getter or setter not provided for: %s" %
                                 name)
        self._prevent_changed = True
        setter(widget, inval)
        self._prevent_changed = False

        self.settings[name] = self.Setting(name, prefs, widget, getter, setter,
                                           inval)
        return widget

    def choices_widget(self,
                       name,
                       choices,
                       fallback_val,
                       none_val,
                       prefs=None):
        prefs = prefs or tprefs
        widget = QComboBox(self)
        widget.currentIndexChanged[int].connect(self.emit_changed)
        for key, human in sorted(choices.iteritems(),
                                 key=lambda (key, human): human or key):
            widget.addItem(human or key, key)

        def getter(w):
            ans = unicode(w.itemData(w.currentIndex()).toString())
            return {none_val: None}.get(ans, ans)

        def setter(w, val):
            val = {None: none_val}.get(val, val)
            idx = w.findData(val,
                             flags=Qt.MatchFixedString | Qt.MatchCaseSensitive)
            if idx == -1:
                idx = w.findData(fallback_val,
                                 flags=Qt.MatchFixedString
                                 | Qt.MatchCaseSensitive)
            w.setCurrentIndex(idx)

        return self(name,
                    widget=widget,
                    getter=getter,
                    setter=setter,
                    prefs=prefs)

    def order_widget(self, name, prefs=None):
        prefs = prefs or tprefs
        widget = QListWidget(self)
        widget.addItems(prefs.defaults[name])
        widget.setDragEnabled(True)
        widget.setDragDropMode(widget.InternalMove)
        widget.viewport().setAcceptDrops(True)
        widget.setDropIndicatorShown(True)
        widget.indexesMoved.connect(self.emit_changed)
        widget.setDefaultDropAction(Qt.MoveAction)
        widget.setMovement(widget.Snap)
        widget.setSpacing(5)
        widget.defaults = prefs.defaults[name]

        def getter(w):
            return list(
                map(unicode, (w.item(i).text() for i in xrange(w.count()))))

        def setter(w, val):
            order_map = {x: i for i, x in enumerate(val)}
            items = list(w.defaults)
            limit = len(items)
            items.sort(key=lambda x: order_map.get(x, limit))
            w.clear()
            for x in items:
                i = QListWidgetItem(w)
                i.setText(x)
                i.setFlags(i.flags() | Qt.ItemIsDragEnabled)

        return self(name,
                    widget=widget,
                    getter=getter,
                    setter=setter,
                    prefs=prefs)

    def emit_changed(self, *args):
        if not self._prevent_changed:
            self.changed_signal.emit()

    def commit(self):
        with tprefs:
            for name in self.settings:
                cv = self.current_value(name)
                if self.initial_value(name) != cv:
                    prefs = self.settings[name].prefs
                    if cv == self.default_value(name):
                        del prefs[name]
                    else:
                        prefs[name] = cv

    def restore_defaults(self):
        for setting in self.settings.itervalues():
            setting.setter(setting.widget, self.default_value(setting.name))

    def initial_value(self, name):
        return self.settings[name].initial_value

    def current_value(self, name):
        s = self.settings[name]
        return s.getter(s.widget)

    def default_value(self, name):
        s = self.settings[name]
        return s.prefs.defaults[name]

    def setting_changed(self, name):
        return self.current_value(name) != self.initial_value(name)
Exemple #18
0
class LibraryPage(QWizardPage, LibraryUI):

    ID = 1
    retranslate = pyqtSignal()

    def __init__(self):
        QWizardPage.__init__(self)
        self.setupUi(self)
        self.registerField('library_location', self.location)
        self.button_change.clicked[()].connect(self.change)
        self.init_languages()
        self.language.currentIndexChanged[int].connect(self.change_language)
        self.location.textChanged.connect(self.location_text_changed)

    def location_text_changed(self, newtext):
        self.completeChanged.emit()

    def init_languages(self):
        self.language.blockSignals(True)
        self.language.clear()
        from calibre.utils.localization import (available_translations,
                                                get_language, get_lang,
                                                get_lc_messages_path)
        lang = get_lang()
        lang = get_lc_messages_path(lang) if lang else lang
        if lang is None or lang not in available_translations():
            lang = 'en'

        def get_esc_lang(l):
            if l == 'en':
                return 'English'
            return get_language(l)

        self.language.addItem(get_esc_lang(lang), QVariant(lang))
        items = [(l, get_esc_lang(l)) for l in available_translations()
                 if l != lang]
        if lang != 'en':
            items.append(('en', get_esc_lang('en')))
        items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
        for item in items:
            self.language.addItem(item[1], QVariant(item[0]))
        self.language.blockSignals(False)
        prefs['language'] = str(
            self.language.itemData(self.language.currentIndex()).toString())

    def change_language(self, idx):
        prefs['language'] = str(
            self.language.itemData(self.language.currentIndex()).toString())
        import __builtin__
        __builtin__.__dict__['_'] = lambda (x): x
        from calibre.utils.localization import set_translators
        from calibre.gui2 import qt_app
        from calibre.ebooks.metadata.book.base import reset_field_metadata
        set_translators()
        qt_app.load_translations()
        self.retranslate.emit()
        self.init_languages()
        reset_field_metadata()
        try:
            lang = prefs['language'].lower()[:2]
            metadata_plugins = {
                'zh': ('Douban Books', ),
                'fr': ('Nicebooks', ),
                'ru': ('OZON.ru', ),
            }.get(lang, [])
            from calibre.customize.ui import enable_plugin
            for name in metadata_plugins:
                enable_plugin(name)
        except:
            pass

    def is_library_dir_suitable(self, x):
        from calibre.db.legacy import LibraryDatabase
        try:
            return LibraryDatabase.exists_at(x) or not os.listdir(x)
        except:
            return False

    def validatePage(self):
        newloc = unicode(self.location.text())
        if not self.is_library_dir_suitable(newloc):
            self.show_library_dir_error(newloc)
            return False
        return True

    def change(self):
        from calibre.db.legacy import LibraryDatabase
        x = choose_dir(self, 'database location dialog',
                       _('Select location for books'))
        if x:
            if (iswindows
                    and len(x) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
                return error_dialog(
                    self,
                    _('Too long'),
                    _('Path to library too long. Must be less than'
                      ' %d characters.') %
                    (LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT),
                    show=True)
            if not os.path.exists(x):
                try:
                    os.makedirs(x)
                except:
                    return error_dialog(self,
                                        _('Bad location'),
                                        _('Failed to create a folder at %s') %
                                        x,
                                        det_msg=traceback.format_exc(),
                                        show=True)

            if self.is_library_dir_suitable(x):
                self.location.setText(x)
            else:
                self.show_library_dir_error(x)

    def show_library_dir_error(self, x):
        if not isinstance(x, unicode):
            try:
                x = x.decode(filesystem_encoding)
            except:
                x = unicode(repr(x))
        error_dialog(self,
                     _('Bad location'),
                     _('You must choose an empty folder for '
                       'the calibre library. %s is not empty.') % x,
                     show=True)

    def initializePage(self):
        lp = prefs['library_path']
        self.default_library_name = None
        if not lp:
            fname = _('Calibre Library')
            base = os.path.expanduser(u'~')
            if iswindows:
                x = winutil.special_folder_path(winutil.CSIDL_PERSONAL)
                if x and os.access(x, os.W_OK):
                    base = x

            lp = os.path.join(base, fname)
            self.default_library_name = lp
            if not os.path.exists(lp):
                try:
                    os.makedirs(lp)
                except:
                    traceback.print_exc()
                    lp = os.path.expanduser(u'~')
        self.location.setText(lp)
        # Hide the library location settings if we are a portable install
        for x in ('location', 'button_change', 'libloc_label1',
                  'libloc_label2'):
            getattr(self, x).setVisible(not isportable)

    def isComplete(self):
        try:
            lp = unicode(self.location.text())
            ans = bool(lp) and os.path.exists(lp) and os.path.isdir(
                lp) and os.access(lp, os.W_OK)
        except:
            ans = False
        return ans

    def commit(self, completed):
        oldloc = prefs['library_path']
        newloc = unicode(self.location.text())
        try:
            dln = self.default_library_name
            if (dln and os.path.exists(dln) and not os.listdir(dln)
                    and newloc != dln):
                os.rmdir(dln)
        except:
            pass
        if not os.path.exists(newloc):
            os.mkdir(newloc)
        if not patheq(oldloc, newloc):
            move_library(oldloc, newloc, self.wizard(), completed)
            return True
        return False

    def nextId(self):
        return DevicePage.ID
Exemple #19
0
class CacheUpdateThread(Thread, QObject):

    total_changed = pyqtSignal(int)
    update_progress = pyqtSignal(int)
    update_details = pyqtSignal(unicode)

    def __init__(self, config, seralize_books_function, timeout):
        Thread.__init__(self)
        QObject.__init__(self)

        self.daemon = True
        self.config = config
        self.seralize_books = seralize_books_function
        self.timeout = timeout
        self._run = True

    def abort(self):
        self._run = False

    def run(self):
        url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'

        self.update_details.emit(_('Checking last download date.'))
        last_download = self.config.get('last_download', None)
        # Don't update the book list if our cache is less than one week old.
        if last_download and (time.time() - last_download) < 604800:
            return

        self.update_details.emit(_('Downloading book list from MobileRead.'))
        # Download the book list HTML file from MobileRead.
        br = browser()
        raw_data = None
        try:
            with closing(br.open(url, timeout=self.timeout)) as f:
                raw_data = f.read()
        except:
            return

        if not raw_data or not self._run:
            return

        self.update_details.emit(_('Processing books.'))
        # Turn books listed in the HTML file into SearchResults's.
        books = []
        try:
            data = html.fromstring(raw_data)
            raw_books = data.xpath('//ul/li')
            self.total_changed.emit(len(raw_books))

            for i, book_data in enumerate(raw_books):
                self.update_details.emit(
                    _('%(num)s of %(tot)s books processed.') %
                    dict(num=i, tot=len(raw_books)))
                book = SearchResult()
                book.detail_item = ''.join(book_data.xpath('.//a/@href'))
                book.formats = ''.join(book_data.xpath('.//i/text()'))
                book.formats = book.formats.strip()

                text = ''.join(book_data.xpath('.//a/text()'))
                if ':' in text:
                    book.author, q, text = text.partition(':')
                book.author = book.author.strip()
                book.title = text.strip()
                books.append(book)

                if not self._run:
                    books = []
                    break
                else:
                    self.update_progress.emit(i)
        except:
            pass

        # Save the book list and it's create time.
        if books:
            self.config['book_list'] = self.seralize_books(books)
            self.config['last_download'] = time.time()
Exemple #20
0
class CoverView(QWidget):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        QWidget.__init__(self, parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        self.pixmap = None
        self.blank = QPixmap(I('blank.png'))
        self.setSizePolicy(QSizePolicy.Preferred,
                           QSizePolicy.GrowFlag | QSizePolicy.ExpandFlag)
        self.sizePolicy().setHeightForWidth(True)

    @property
    def is_blank(self):
        return self.pixmap is None

    @dynamic_property
    def current_val(self):
        def fget(self):
            return self.pixmap

        def fset(self, val):
            self.pixmap = val
            self.changed.emit()
            self.update()

        return property(fget=fget, fset=fset)

    def from_mi(self, mi):
        p = getattr(mi, 'cover', None)
        if p and os.path.exists(p):
            pmap = QPixmap()
            with open(p, 'rb') as f:
                pmap.loadFromData(f.read())
            if not pmap.isNull():
                self.pixmap = pmap
                self.update()
                self.changed.emit()
                return
        cd = getattr(mi, 'cover_data', (None, None))
        if cd and cd[1]:
            pmap = QPixmap()
            pmap.loadFromData(cd[1])
            if not pmap.isNull():
                self.pixmap = pmap
                self.update()
                self.changed.emit()
                return
        self.pixmap = None
        self.update()
        self.changed.emit()

    def to_mi(self, mi):
        mi.cover, mi.cover_data = None, (None, None)
        if self.pixmap is not None and not self.pixmap.isNull():
            with PersistentTemporaryFile('.jpg') as pt:
                pt.write(pixmap_to_data(self.pixmap))
                mi.cover = pt.name

    def same_as(self, other):
        return self.current_val == other.current_val

    def sizeHint(self):
        return QSize(225, 300)

    def paintEvent(self, event):
        pmap = self.blank if self.pixmap is None or self.pixmap.isNull(
        ) else self.pixmap
        target = self.rect()
        scaled, width, height = fit_image(pmap.width(), pmap.height(),
                                          target.width(), target.height())
        target.setRect(target.x(), target.y(), width, height)
        p = QPainter(self)
        p.setRenderHints(QPainter.Antialiasing
                         | QPainter.SmoothPixmapTransform)
        p.drawPixmap(target, pmap)

        if self.pixmap is not None and not self.pixmap.isNull():
            sztgt = target.adjusted(0, 0, 0, -4)
            f = p.font()
            f.setBold(True)
            p.setFont(f)
            sz = u'\u00a0%d x %d\u00a0' % (self.pixmap.width(),
                                           self.pixmap.height())
            flags = Qt.AlignBottom | Qt.AlignRight | Qt.TextSingleLine
            szrect = p.boundingRect(sztgt, flags, sz)
            p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
            p.setPen(QPen(QColor(255, 255, 255)))
            p.drawText(sztgt, flags, sz)
        p.end()
Exemple #21
0
class ResultsView(QTableView):  # {{{

    show_details_signal = pyqtSignal(object)
    book_selected = pyqtSignal(object)

    def __init__(self, parent=None):
        QTableView.__init__(self, parent)
        self.rt_delegate = RichTextDelegate(self)
        self.setSelectionMode(self.SingleSelection)
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(self.SelectRows)
        self.setIconSize(QSize(24, 24))
        self.clicked.connect(self.show_details)
        self.doubleClicked.connect(self.select_index)
        self.setSortingEnabled(True)

    def show_results(self, results):
        self._model = ResultsModel(results, self)
        self.setModel(self._model)
        for i in self._model.HTML_COLS:
            self.setItemDelegateForColumn(i, self.rt_delegate)
        self.resizeRowsToContents()
        self.resizeColumnsToContents()
        self.setFocus(Qt.OtherFocusReason)
        idx = self.model().index(0, 0)
        if idx.isValid() and self.model().rowCount() > 0:
            self.show_details(idx)
            sm = self.selectionModel()
            sm.select(idx, sm.ClearAndSelect | sm.Rows)

    def resize_delegate(self):
        self.rt_delegate.max_width = int(self.width() / 2.1)
        self.resizeColumnsToContents()

    def resizeEvent(self, ev):
        ret = super(ResultsView, self).resizeEvent(ev)
        self.resize_delegate()
        return ret

    def currentChanged(self, current, previous):
        ret = QTableView.currentChanged(self, current, previous)
        self.show_details(current)
        return ret

    def show_details(self, index):
        f = rating_font()
        book = self.model().data(index, Qt.UserRole)
        parts = [
            '<center>',
            '<h2>%s</h2>' % book.title,
            '<div><i>%s</i></div>' % authors_to_string(book.authors),
        ]
        if not book.is_null('series'):
            series = book.format_field('series')
            if series[1]:
                parts.append('<div>%s: %s</div>' % series)
        if not book.is_null('rating'):
            style = 'style=\'font-family:"%s"\'' % f
            parts.append('<div %s>%s</div>' %
                         (style, '\u2605' * int(book.rating)))
        parts.append('</center>')
        if book.identifiers:
            urls = urls_from_identifiers(book.identifiers)
            ids = [
                '<a href="%s">%s</a>' % (url, name)
                for name, ign, ign, url in urls
            ]
            if ids:
                parts.append('<div><b>%s:</b> %s</div><br>' %
                             (_('See at'), ', '.join(ids)))
        if book.tags:
            parts.append('<div>%s</div><div>\u00a0</div>' %
                         ', '.join(book.tags))
        if book.comments:
            parts.append(comments_to_html(book.comments))

        self.show_details_signal.emit(''.join(parts))

    def select_index(self, index):
        if self.model() is None:
            return
        if not index.isValid():
            index = self.model().index(0, 0)
        book = self.model().data(index, Qt.UserRole)
        self.book_selected.emit(book)

    def get_result(self):
        self.select_index(self.currentIndex())
Exemple #22
0
class LineEdit(QLineEdit):

    changed = pyqtSignal()

    def __init__(self, field, is_new, parent, metadata, extra):
        QLineEdit.__init__(self, parent)
        self.is_new = is_new
        self.field = field
        self.metadata = metadata
        if not is_new:
            self.setReadOnly(True)
        self.textChanged.connect(self.changed)

    def from_mi(self, mi):
        val = mi.get(self.field, default='') or ''
        ism = self.metadata['is_multiple']
        if ism:
            if not val:
                val = ''
            else:
                val = ism['list_to_ui'].join(val)
        self.setText(val)
        self.setCursorPosition(0)

    def to_mi(self, mi):
        val = unicode(self.text()).strip()
        ism = self.metadata['is_multiple']
        if ism:
            if not val:
                val = []
            else:
                val = [
                    x.strip() for x in val.split(ism['list_to_ui'])
                    if x.strip()
                ]
        mi.set(self.field, val)
        if self.field == 'title':
            mi.set('title_sort', title_sort(val, lang=mi.language))
        elif self.field == 'authors':
            mi.set('author_sort', authors_to_sort_string(val))

    @dynamic_property
    def current_val(self):
        def fget(self):
            return unicode(self.text())

        def fset(self, val):
            self.setText(val)
            self.setCursorPosition(0)

        return property(fget=fget, fset=fset)

    @property
    def is_blank(self):
        val = self.current_val.strip()
        if self.field in {'title', 'authors'}:
            return val in {'', _('Unknown')}
        return not val

    def same_as(self, other):
        return self.current_val == other.current_val
Exemple #23
0
class CoversView(QListView):  # {{{

    chosen = pyqtSignal()

    def __init__(self, current_cover, parent=None):
        QListView.__init__(self, parent)
        self.m = CoversModel(current_cover, self)
        self.setModel(self.m)

        self.setFlow(self.LeftToRight)
        self.setWrapping(True)
        self.setResizeMode(self.Adjust)
        self.setGridSize(QSize(190, 260))
        self.setIconSize(QSize(150, 200))
        self.setSelectionMode(self.SingleSelection)
        self.setViewMode(self.IconMode)

        self.delegate = CoverDelegate(self)
        self.setItemDelegate(self.delegate)
        self.delegate.needs_redraw.connect(self.viewport().update,
                                           type=Qt.QueuedConnection)

        self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)

    def select(self, num):
        current = self.model().index(num)
        sm = self.selectionModel()
        sm.select(current, sm.SelectCurrent)

    def start(self):
        self.select(0)
        self.delegate.start_animation()

    def reset_covers(self):
        self.m.reset_covers()

    def clear_failed(self):
        pointer = self.m.pointer_from_index(self.currentIndex())
        self.m.clear_failed()
        if pointer is None:
            self.select(0)
        else:
            self.select(self.m.index_from_pointer(pointer).row())

    def show_context_menu(self, point):
        idx = self.currentIndex()
        if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject():
            m = QMenu()
            m.addAction(QIcon(I('view.png')),
                        _('View this cover at full size'), self.show_cover)
            m.addAction(QIcon(I('edit-copy.png')),
                        _('Copy this cover to clipboard'), self.copy_cover)
            m.exec_(QCursor.pos())

    def show_cover(self):
        idx = self.currentIndex()
        pmap = self.model().cover_pixmap(idx)
        if pmap is None and idx.row() == 0:
            pmap = self.model().cc
        if pmap is not None:
            from calibre.gui2.viewer.image_popup import ImageView
            d = ImageView(self,
                          pmap,
                          unicode(idx.data(Qt.DisplayRole).toString()),
                          geom_name='metadata_download_cover_popup_geom')
            d(use_exec=True)

    def copy_cover(self):
        idx = self.currentIndex()
        pmap = self.model().cover_pixmap(idx)
        if pmap is None and idx.row() == 0:
            pmap = self.model().cc
        if pmap is not None:
            QApplication.clipboard().setPixmap(pmap)
Exemple #24
0
class Rule(QWidget):

    remove = pyqtSignal(object)

    def __init__(self, device, rule=None):
        QWidget.__init__(self)
        self._device = weakref.ref(device)

        self.l = l = QHBoxLayout()
        self.setLayout(l)

        p, s = _('Send the %s format to the folder:').partition('%s')[0::2]
        self.l1 = l1 = QLabel(p)
        l.addWidget(l1)
        self.fmt = f = QComboBox(self)
        l.addWidget(f)
        self.l2 = l2 = QLabel(s)
        l.addWidget(l2)
        self.folder = f = QLineEdit(self)
        f.setPlaceholderText(_('Folder on the device'))
        l.addWidget(f)
        self.b = b = QToolButton()
        l.addWidget(b)
        b.setIcon(QIcon(I('document_open.png')))
        b.clicked.connect(self.browse)
        b.setToolTip(_('Browse for a folder on the device'))
        self.rb = rb = QPushButton(QIcon(I('list_remove.png')),
                                   _('&Remove rule'), self)
        l.addWidget(rb)
        rb.clicked.connect(self.removed)

        for fmt in sorted(BOOK_EXTENSIONS):
            self.fmt.addItem(fmt.upper(), fmt.lower())

        self.fmt.setCurrentIndex(0)

        if rule is not None:
            fmt, folder = rule
            idx = self.fmt.findText(fmt.upper())
            if idx > -1:
                self.fmt.setCurrentIndex(idx)
            self.folder.setText(folder)

        self.ignore = False

    @property
    def device(self):
        return self._device()

    def browse(self):
        b = Browser(self.device.filesystem_cache,
                    show_files=False,
                    parent=self)
        if b.exec_() == b.Accepted and b.current_item is not None:
            sid, path = b.current_item
            self.folder.setText('/'.join(path[1:]))

    def removed(self):
        self.remove.emit(self)

    @property
    def rule(self):
        folder = unicode(self.folder.text()).strip()
        if folder:
            return (unicode(
                self.fmt.itemData(self.fmt.currentIndex()).toString()), folder)
        return None
Exemple #25
0
class CoversWidget(QWidget):  # {{{

    chosen = pyqtSignal()
    finished = pyqtSignal()

    def __init__(self, log, current_cover, parent=None):
        QWidget.__init__(self, parent)
        self.log = log
        self.abort = Event()

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

        self.msg = QLabel()
        self.msg.setWordWrap(True)
        l.addWidget(self.msg, 0, 0)

        self.covers_view = CoversView(current_cover, self)
        self.covers_view.chosen.connect(self.chosen)
        l.addWidget(self.covers_view, 1, 0)
        self.continue_processing = True

    def reset_covers(self):
        self.covers_view.reset_covers()

    def start(self, book, current_cover, title, authors, caches):
        self.continue_processing = True
        self.abort.clear()
        self.book, self.current_cover = book, current_cover
        self.title, self.authors = title, authors
        self.log('Starting cover download for:', book.title)
        self.log('Query:', title, authors, self.book.identifiers)
        self.msg.setText(
            '<p>' +
            _('Downloading covers for <b>%s</b>, please wait...') % book.title)
        self.covers_view.start()

        self.worker = CoverWorker(self.log, self.abort, self.title,
                                  self.authors, book.identifiers, caches)
        self.worker.start()
        QTimer.singleShot(50, self.check)
        self.covers_view.setFocus(Qt.OtherFocusReason)

    def check(self):
        if self.worker.is_alive() and not self.abort.is_set():
            QTimer.singleShot(50, self.check)
            try:
                self.process_result(self.worker.rq.get_nowait())
            except Empty:
                pass
        else:
            self.process_results()

    def process_results(self):
        while self.continue_processing:
            try:
                self.process_result(self.worker.rq.get_nowait())
            except Empty:
                break

        if self.continue_processing:
            self.covers_view.clear_failed()

        if self.worker.error is not None:
            error_dialog(self,
                         _('Download failed'),
                         _('Failed to download any covers, click'
                           ' "Show details" for details.'),
                         det_msg=self.worker.error,
                         show=True)

        num = self.covers_view.model().rowCount()
        if num < 2:
            txt = _(
                'Could not find any covers for <b>%s</b>') % self.book.title
        else:
            txt = _(
                'Found <b>%(num)d</b> possible covers for %(title)s. '
                'When the download completes, the covers will be sorted by size.'
            ) % dict(num=num - 1, title=self.title)
        self.msg.setText(txt)
        self.msg.setWordWrap(True)

        self.finished.emit()

    def process_result(self, result):
        if not self.continue_processing:
            return
        plugin_name, width, height, fmt, data = result
        self.covers_view.model().update_result(plugin_name, width, height,
                                               data)

    def cleanup(self):
        self.covers_view.delegate.stop_animation()
        self.continue_processing = False

    def cancel(self):
        self.cleanup()
        self.abort.set()

    def cover_pixmap(self):
        idx = None
        for i in self.covers_view.selectionModel().selectedIndexes():
            if i.isValid():
                idx = i
                break
        if idx is None:
            idx = self.covers_view.currentIndex()
        return self.covers_view.model().cover_pixmap(idx)
Exemple #26
0
class BookInfo(QDialog):

    closed = pyqtSignal(object)

    def __init__(self, parent, view, row, link_delegate):
        QDialog.__init__(self, parent)
        self.normal_brush = QBrush(Qt.white)
        self.marked_brush = QBrush(Qt.lightGray)
        self.marked = None
        self.gui = parent
        self.splitter = QSplitter(self)
        self._l = l = QVBoxLayout(self)
        self.setLayout(l)
        l.addWidget(self.splitter)

        self.cover = CoverView(self)
        self.cover.resizeEvent = self.cover_view_resized
        self.cover.cover_changed.connect(self.cover_changed)
        self.cover_pixmap = None
        self.cover.sizeHint = self.details_size_hint
        self.splitter.addWidget(self.cover)

        self.details = QWebView(self)
        self.details.sizeHint = self.details_size_hint
        self.details.page().setLinkDelegationPolicy(
            self.details.page().DelegateAllLinks)
        self.details.linkClicked.connect(self.link_clicked)
        self.css = css()
        self.link_delegate = link_delegate
        self.details.setAttribute(Qt.WA_OpaquePaintEvent, False)
        palette = self.details.palette()
        self.details.setAcceptDrops(False)
        palette.setBrush(QPalette.Base, Qt.transparent)
        self.details.page().setPalette(palette)

        self.c = QWidget(self)
        self.c.l = l2 = QGridLayout(self.c)
        self.c.setLayout(l2)
        l2.addWidget(self.details, 0, 0, 1, -1)
        self.splitter.addWidget(self.c)

        self.fit_cover = QCheckBox(_('Fit &cover within view'), self)
        self.fit_cover.setChecked(
            gprefs.get('book_info_dialog_fit_cover', True))
        l2.addWidget(self.fit_cover, l2.rowCount(), 0, 1, -1)
        self.previous_button = QPushButton(QIcon(I('previous.png')),
                                           _('&Previous'), self)
        self.previous_button.clicked.connect(self.previous)
        l2.addWidget(self.previous_button, l2.rowCount(), 0)
        self.next_button = QPushButton(QIcon(I('next.png')), _('&Next'), self)
        self.next_button.clicked.connect(self.next)
        l2.addWidget(self.next_button, l2.rowCount() - 1, 1)

        self.view = view
        self.current_row = None
        self.refresh(row)
        self.view.selectionModel().currentChanged.connect(self.slave)
        self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
        self.ns = QShortcut(QKeySequence('Alt+Right'), self)
        self.ns.activated.connect(self.next)
        self.ps = QShortcut(QKeySequence('Alt+Left'), self)
        self.ps.activated.connect(self.previous)
        self.next_button.setToolTip(
            _('Next [%s]') %
            unicode(self.ns.key().toString(QKeySequence.NativeText)))
        self.previous_button.setToolTip(
            _('Previous [%s]') %
            unicode(self.ps.key().toString(QKeySequence.NativeText)))

        geom = QCoreApplication.instance().desktop().availableGeometry(self)
        screen_height = geom.height() - 100
        screen_width = geom.width() - 100
        self.resize(max(int(screen_width / 2), 700), screen_height)
        saved_layout = gprefs.get('book_info_dialog_layout', None)
        if saved_layout is not None:
            try:
                self.restoreGeometry(saved_layout[0])
                self.splitter.restoreState(saved_layout[1])
            except Exception:
                pass

    def link_clicked(self, qurl):
        link = unicode(qurl.toString())
        self.link_delegate(link)

    def done(self, r):
        saved_layout = (bytearray(self.saveGeometry()),
                        bytearray(self.splitter.saveState()))
        gprefs.set('book_info_dialog_layout', saved_layout)
        ret = QDialog.done(self, r)
        self.view.selectionModel().currentChanged.disconnect(self.slave)
        self.view = self.link_delegate = self.gui = None
        self.closed.emit(self)
        return ret

    def cover_changed(self, data):
        if self.current_row is not None:
            id_ = self.view.model().id(self.current_row)
            self.view.model().db.set_cover(id_, data)
        if self.gui.cover_flow:
            self.gui.cover_flow.dataChanged()
        ci = self.view.currentIndex()
        if ci.isValid():
            self.view.model().current_changed(ci, ci)
        self.cover_pixmap = QPixmap()
        self.cover_pixmap.loadFromData(data)
        if self.fit_cover.isChecked():
            self.resize_cover()

    def details_size_hint(self):
        return QSize(350, 550)

    def toggle_cover_fit(self, state):
        gprefs.set('book_info_dialog_fit_cover', self.fit_cover.isChecked())
        self.resize_cover()

    def cover_view_resized(self, event):
        QTimer.singleShot(1, self.resize_cover)

    def slave(self, current, previous):
        if current.row() != previous.row():
            row = current.row()
            self.refresh(row)

    def move(self, delta=1):
        self.view.selectionModel().currentChanged.disconnect(self.slave)
        try:
            idx = self.view.currentIndex()
            if idx.isValid():
                m = self.view.model()
                ni = m.index(idx.row() + delta, idx.column())
                if ni.isValid():
                    self.view.setCurrentIndex(ni)
                    self.refresh(ni.row())
                    if self.view.isVisible():
                        self.view.scrollTo(ni)
        finally:
            self.view.selectionModel().currentChanged.connect(self.slave)

    def next(self):
        self.move()

    def previous(self):
        self.move(-1)

    def resize_cover(self):
        if self.cover_pixmap is None:
            return
        pixmap = self.cover_pixmap
        if self.fit_cover.isChecked():
            scaled, new_width, new_height = fit_image(
                pixmap.width(), pixmap.height(),
                self.cover.size().width() - 10,
                self.cover.size().height() - 10)
            if scaled:
                pixmap = pixmap.scaled(new_width, new_height,
                                       Qt.KeepAspectRatio,
                                       Qt.SmoothTransformation)
        self.cover.set_pixmap(pixmap)
        self.update_cover_tooltip()

    def update_cover_tooltip(self):
        tt = ''
        if self.marked:
            tt = _('This book is marked') if self.marked in {
                True, 'true'
            } else _('This book is marked as: %s') % self.marked
            tt += '\n\n'
        if self.cover_pixmap is not None:
            sz = self.cover_pixmap.size()
            tt += _('Cover size: %(width)d x %(height)d') % dict(
                width=sz.width(), height=sz.height())
        self.cover.setToolTip(tt)

    def refresh(self, row):
        if isinstance(row, QModelIndex):
            row = row.row()
        if row == self.current_row:
            return
        mi = self.view.model().get_book_display_info(row)
        if mi is None:
            # Indicates books was deleted from library, or row numbers have
            # changed
            return

        self.previous_button.setEnabled(False if row == 0 else True)
        self.next_button.setEnabled(False if row ==
                                    self.view.model().rowCount(QModelIndex()) -
                                    1 else True)
        self.current_row = row
        self.setWindowTitle(mi.title)
        self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1])
        self.resize_cover()
        html = render_html(mi, self.css, True, self, all_fields=True)
        self.details.setHtml(html)
        self.marked = mi.marked
        self.cover.setBackgroundBrush(
            self.marked_brush if mi.marked else self.normal_brush)
        self.update_cover_tooltip()
class FileList(QTreeWidget):

    delete_requested = pyqtSignal(object, object)
    reorder_spine = pyqtSignal(object)
    rename_requested = pyqtSignal(object, object)
    bulk_rename_requested = pyqtSignal(object)
    edit_file = pyqtSignal(object, object, object)
    merge_requested = pyqtSignal(object, object, object)
    mark_requested = pyqtSignal(object, object)
    export_requested = pyqtSignal(object, object)
    replace_requested = pyqtSignal(object, object, object, object)
    link_stylesheets_requested = pyqtSignal(object, object, object)

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)
        pi = plugins['progress_indicator'][0]
        if hasattr(pi, 'set_no_activate_on_click'):
            pi.set_no_activate_on_click(self)
        self.current_edited_name = None
        self.delegate = ItemDelegate(self)
        self.delegate.rename_requested.connect(self.rename_requested)
        self.setTextElideMode(Qt.ElideMiddle)
        self.setItemDelegate(self.delegate)
        self.setIconSize(QSize(16, 16))
        self.header().close()
        self.setDragEnabled(True)
        self.setEditTriggers(self.EditKeyPressed)
        self.setSelectionMode(self.ExtendedSelection)
        self.viewport().setAcceptDrops(True)
        self.setDropIndicatorShown(True)
        self.setDragDropMode(self.InternalMove)
        self.setAutoScroll(True)
        self.setAutoScrollMargin(TOP_ICON_SIZE * 2)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setAutoExpandDelay(1000)
        self.setAnimated(True)
        self.setMouseTracking(True)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.root = self.invisibleRootItem()
        self.emblem_cache = {}
        self.rendered_emblem_cache = {}
        self.top_level_pixmap_cache = {
            name:
            QPixmap(I(icon)).scaled(TOP_ICON_SIZE,
                                    TOP_ICON_SIZE,
                                    transformMode=Qt.SmoothTransformation)
            for name, icon in {
                'text': 'keyboard-prefs.png',
                'styles': 'lookfeel.png',
                'fonts': 'font.png',
                'misc': 'mimetypes/dir.png',
                'images': 'view-image.png',
            }.iteritems()
        }
        self.itemActivated.connect(self.item_double_clicked)

    def get_state(self):
        s = {'pos': self.verticalScrollBar().value()}
        s['expanded'] = {
            c
            for c, item in self.categories.iteritems() if item.isExpanded()
        }
        s['selected'] = {
            unicode(i.data(0, NAME_ROLE).toString())
            for i in self.selectedItems()
        }
        return s

    def set_state(self, state):
        for category, item in self.categories.iteritems():
            item.setExpanded(category in state['expanded'])
        self.verticalScrollBar().setValue(state['pos'])
        for parent in self.categories.itervalues():
            for c in (parent.child(i) for i in xrange(parent.childCount())):
                name = unicode(c.data(0, NAME_ROLE).toString())
                if name in state['selected']:
                    c.setSelected(True)

    def item_from_name(self, name):
        for parent in self.categories.itervalues():
            for c in (parent.child(i) for i in xrange(parent.childCount())):
                q = unicode(c.data(0, NAME_ROLE).toString())
                if q == name:
                    return c

    def select_name(self, name):
        for parent in self.categories.itervalues():
            for c in (parent.child(i) for i in xrange(parent.childCount())):
                q = unicode(c.data(0, NAME_ROLE).toString())
                c.setSelected(q == name)
                if q == name:
                    self.scrollToItem(c)

    def mark_name_as_current(self, name):
        current = self.item_from_name(name)
        if current is not None:
            if self.current_edited_name is not None:
                ci = self.item_from_name(self.current_edited_name)
                if ci is not None:
                    ci.setData(0, Qt.FontRole, None)
            self.current_edited_name = name
            self.mark_item_as_current(current)

    def mark_item_as_current(self, item):
        font = QFont(self.font())
        font.setItalic(True)
        font.setBold(True)
        item.setData(0, Qt.FontRole, font)

    def clear_currently_edited_name(self):
        if self.current_edited_name:
            ci = self.item_from_name(self.current_edited_name)
            if ci is not None:
                ci.setData(0, Qt.FontRole, None)
        self.current_edited_name = None

    def build(self, container, preserve_state=True):
        if preserve_state:
            state = self.get_state()
        self.clear()
        self.root = self.invisibleRootItem()
        self.root.setFlags(Qt.ItemIsDragEnabled)
        self.categories = {}
        for category, text in (
            ('text', _('Text')),
            ('styles', _('Styles')),
            ('images', _('Images')),
            ('fonts', _('Fonts')),
            ('misc', _('Miscellaneous')),
        ):
            self.categories[category] = i = QTreeWidgetItem(self.root, 0)
            i.setText(0, text)
            i.setData(0, Qt.DecorationRole,
                      self.top_level_pixmap_cache[category])
            f = i.font(0)
            f.setBold(True)
            i.setFont(0, f)
            i.setData(0, NAME_ROLE, category)
            flags = Qt.ItemIsEnabled
            if category == 'text':
                flags |= Qt.ItemIsDropEnabled
            i.setFlags(flags)

        processed, seen = {}, {}

        cover_page_name = get_cover_page_name(container)
        cover_image_name = get_raster_cover_name(container)
        manifested_names = set()
        for names in container.manifest_type_map.itervalues():
            manifested_names |= set(names)

        def get_category(name, mt):
            category = 'misc'
            if mt.startswith('image/'):
                category = 'images'
            elif mt in OEB_FONTS:
                category = 'fonts'
            elif mt in OEB_STYLES:
                category = 'styles'
            elif mt in OEB_DOCS:
                category = 'text'
            ext = name.rpartition('.')[-1].lower()
            if ext in {'ttf', 'otf', 'woff'}:
                # Probably wrong mimetype in the OPF
                category = 'fonts'
            return category

        def set_display_name(name, item):
            if name in processed:
                # We have an exact duplicate (can happen if there are
                # duplicates in the spine)
                item.setText(0, processed[name].text(0))
                item.setText(1, processed[name].text(1))
                return

            parts = name.split('/')
            text = parts[-1]
            while text in seen and parts:
                text = parts.pop() + '/' + text
            seen[text] = item
            item.setText(0, text)
            item.setText(1, hexlify(sort_key(text)))

        def render_emblems(item, emblems):
            emblems = tuple(emblems)
            if not emblems:
                return
            icon = self.rendered_emblem_cache.get(emblems, None)
            if icon is None:
                pixmaps = []
                for emblem in emblems:
                    pm = self.emblem_cache.get(emblem, None)
                    if pm is None:
                        pm = self.emblem_cache[emblem] = QPixmap(
                            I(emblem)).scaled(
                                self.iconSize(),
                                transformMode=Qt.SmoothTransformation)
                    pixmaps.append(pm)
                num = len(pixmaps)
                w, h = pixmaps[0].width(), pixmaps[0].height()
                if num == 1:
                    icon = self.rendered_emblem_cache[emblems] = QIcon(
                        pixmaps[0])
                else:
                    canvas = QPixmap((num * w) + ((num - 1) * 2), h)
                    canvas.fill(Qt.transparent)
                    painter = QPainter(canvas)
                    for i, pm in enumerate(pixmaps):
                        painter.drawPixmap(i * (w + 2), 0, pm)
                    painter.end()
                    icon = self.rendered_emblem_cache[emblems] = canvas
            item.setData(0, Qt.DecorationRole, icon)

        cannot_be_renamed = container.names_that_must_not_be_changed
        ncx_mime = guess_type('a.ncx')

        def create_item(name, linear=None):
            imt = container.mime_map.get(name, guess_type(name))
            icat = get_category(name, imt)
            category = 'text' if linear is not None else ({
                'text': 'misc'
            }.get(icat, icat))
            item = QTreeWidgetItem(
                self.categories['text' if linear is not None else category], 1)
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if category == 'text':
                flags |= Qt.ItemIsDragEnabled
            if name not in cannot_be_renamed:
                flags |= Qt.ItemIsEditable
            item.setFlags(flags)
            item.setStatusTip(0, _('Full path: ') + name)
            item.setData(0, NAME_ROLE, name)
            item.setData(0, CATEGORY_ROLE, category)
            item.setData(0, LINEAR_ROLE, bool(linear))
            item.setData(0, MIME_ROLE, imt)
            set_display_name(name, item)
            tooltips = []
            emblems = []
            if name in {cover_page_name, cover_image_name}:
                emblems.append('default_cover.png')
                tooltips.append(
                    _('This file is the cover %s for this book') %
                    (_('image') if name == cover_image_name else _('page')))
            if name in container.opf_name:
                emblems.append('metadata.png')
                tooltips.append(
                    _('This file contains all the metadata and book structure information'
                      ))
            if imt == ncx_mime:
                emblems.append('toc.png')
                tooltips.append(
                    _('This file contains the metadata table of contents'))
            if name not in manifested_names and not container.ok_to_be_unmanifested(
                    name):
                emblems.append('dialog_question.png')
                tooltips.append(
                    _('This file is not listed in the book manifest'))
            if linear is False:
                emblems.append('arrow-down.png')
                tooltips.append(
                    _('This file is marked as non-linear in the spine\nDrag it to the top to make it linear'
                      ))
            if linear is None and icat == 'text':
                # Text item outside spine
                emblems.append('dialog_warning.png')
                tooltips.append(
                    _('This file is a text file that is not referenced in the spine'
                      ))
            if category == 'text' and name in processed:
                # Duplicate entry in spine
                emblems.append('dialog_error.png')
                tooltips.append(
                    _('This file occurs more than once in the spine'))

            render_emblems(item, emblems)
            if tooltips:
                item.setData(0, Qt.ToolTipRole, '\n'.join(tooltips))
            return item

        for name, linear in container.spine_names:
            processed[name] = create_item(name, linear=linear)

        for name in container.name_path_map:
            if name in processed:
                continue
            processed[name] = create_item(name)

        for name, c in self.categories.iteritems():
            c.setExpanded(True)
            if name != 'text':
                c.sortChildren(1, Qt.AscendingOrder)

        if preserve_state:
            self.set_state(state)

        if self.current_edited_name:
            item = self.item_from_name(self.current_edited_name)
            if item is not None:
                self.mark_item_as_current(item)

    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in set(self.categories.itervalues()):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        container = current_container()
        ci = self.currentItem()
        if ci is not None:
            cn = unicode(ci.data(0, NAME_ROLE).toString())
            mt = unicode(ci.data(0, MIME_ROLE).toString())
            cat = unicode(ci.data(0, CATEGORY_ROLE).toString())
            n = elided_text(cn.rpartition('/')[-1])
            m.addAction(QIcon(I('save.png')),
                        _('Export %s') % n, partial(self.export, cn))
            if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS:
                m.addAction(
                    _('Replace %s with file...') % n,
                    partial(self.replace, cn))
            m.addSeparator()

            m.addAction(QIcon(I('modified.png')),
                        _('&Rename %s') % n, self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover image') % n,
                            partial(self.mark_as_cover, cn))
            elif current_container(
            ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover page') % n,
                            partial(self.mark_as_titlepage, cn))
            m.addSeparator()

        if num > 0:
            m.addSeparator()
            if num > 1:
                m.addAction(QIcon(I('modified.png')),
                            _('&Bulk rename selected files'),
                            self.request_bulk_rename)
            m.addAction(QIcon(I('trash.png')),
                        _('&Delete the %d selected file(s)') % num,
                        self.request_delete)
            m.addSeparator()

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode(item.data(
                0, CATEGORY_ROLE).toString())].append(
                    unicode(item.data(0, NAME_ROLE).toString()))

        for items in selected_map.itervalues():
            items.sort(key=self.index_of_name)

        if selected_map['text']:
            m.addAction(QIcon(I('format-text-color.png')),
                        _('Link &stylesheets...'),
                        partial(self.link_stylesheets, selected_map['text']))

        if len(selected_map['text']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected text files'),
                partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected style files'),
                partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))

    def index_of_name(self, name):
        for category, parent in self.categories.iteritems():
            for i in xrange(parent.childCount()):
                item = parent.child(i)
                if unicode(item.data(0, NAME_ROLE).toString()) == name:
                    return (category, i)
        return (None, -1)

    def start_merge(self, category, names):
        d = MergeDialog(names, self)
        if d.exec_() == d.Accepted and d.ans:
            self.merge_requested.emit(category, names, d.ans)

    def edit_current_item(self):
        if self.currentItem() is not None:
            self.editItem(self.currentItem())

    def mark_as_cover(self, name):
        self.mark_requested.emit(name, 'cover')

    def mark_as_titlepage(self, name):
        first = unicode(self.categories['text'].child(0).data(
            0, NAME_ROLE).toString()) == name
        move_to_start = False
        if not first:
            move_to_start = question_dialog(
                self, _('Not first item'),
                _('%s is not the first text item. You should only mark the'
                  ' first text item as cover. Do you want to make it the'
                  ' first item?') % elided_text(name))
        self.mark_requested.emit(name, 'titlepage:%r' % move_to_start)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
            ev.accept()
            self.request_delete()
        else:
            return QTreeWidget.keyPressEvent(self, ev)

    def request_bulk_rename(self):
        names = {
            unicode(item.data(0, NAME_ROLE).toString())
            for item in self.selectedItems()
        }
        bad = names & current_container().names_that_must_not_be_changed
        if bad:
            return error_dialog(self,
                                _('Cannot rename'),
                                _('The file(s) %s cannot be renamed.') %
                                ('<b>%s</b>' % ', '.join(bad)),
                                show=True)
        names = sorted(names, key=self.index_of_name)
        fmt, num = get_bulk_rename_settings(self, len(names))
        if fmt is not None:

            def change_name(name, num):
                parts = name.split('/')
                base, ext = parts[-1].rpartition('.')[0::2]
                parts[-1] = (fmt % num) + '.' + ext
                return '/'.join(parts)

            name_map = {
                n: change_name(n, num + i)
                for i, n in enumerate(names)
            }
            self.bulk_rename_requested.emit(name_map)

    def request_delete(self):
        names = {
            unicode(item.data(0, NAME_ROLE).toString())
            for item in self.selectedItems()
        }
        bad = names & current_container().names_that_must_not_be_removed
        if bad:
            return error_dialog(self,
                                _('Cannot delete'),
                                _('The file(s) %s cannot be deleted.') %
                                ('<b>%s</b>' % ', '.join(bad)),
                                show=True)

        text = self.categories['text']
        children = (text.child(i) for i in xrange(text.childCount()))
        spine_removals = [(unicode(item.data(0, NAME_ROLE).toString()),
                           item.isSelected()) for item in children]
        other_removals = {
            unicode(item.data(0, NAME_ROLE).toString())
            for item in self.selectedItems()
            if unicode(item.data(0, CATEGORY_ROLE).toString()) != 'text'
        }
        self.delete_requested.emit(spine_removals, other_removals)

    def delete_done(self, spine_removals, other_removals):
        removals = []
        for i, (name, remove) in enumerate(spine_removals):
            if remove:
                removals.append(self.categories['text'].child(i))
        for category, parent in self.categories.iteritems():
            if category != 'text':
                for i in xrange(parent.childCount()):
                    child = parent.child(i)
                    if unicode(child.data(
                            0, NAME_ROLE).toString()) in other_removals:
                        removals.append(child)

        # The sorting by index is necessary otherwise Qt crashes with recursive
        # repaint detected message
        for c in sorted(removals,
                        key=lambda x: x.parent().indexOfChild(x),
                        reverse=True):
            sip.delete(c)

        # A bug in the raster paint engine on linux causes a crash if the scrollbar
        # is at the bottom and the delete happens to cause the scrollbar to
        # update
        b = self.verticalScrollBar()
        if b.value() == b.maximum():
            b.setValue(b.minimum())
            QTimer.singleShot(0, lambda: b.setValue(b.maximum()))

    def dropEvent(self, event):
        text = self.categories['text']
        pre_drop_order = {text.child(i): i for i in xrange(text.childCount())}
        super(FileList, self).dropEvent(event)
        current_order = {text.child(i): i for i in xrange(text.childCount())}
        if current_order != pre_drop_order:
            order = []
            for child in (text.child(i) for i in xrange(text.childCount())):
                name = unicode(child.data(0, NAME_ROLE).toString())
                linear = child.data(0, LINEAR_ROLE).toBool()
                order.append([name, linear])
            # Ensure that all non-linear items are at the end, any non-linear
            # items not at the end will be made linear
            for i, (name, linear) in tuple(enumerate(order)):
                if not linear and i < len(order) - 1 and order[i + 1][1]:
                    order[i][1] = True
            self.reorder_spine.emit(order)

    def item_double_clicked(self, item, column):
        category = unicode(item.data(0, CATEGORY_ROLE).toString())
        if category:
            self._request_edit(item)

    def _request_edit(self, item):
        category = unicode(item.data(0, CATEGORY_ROLE).toString())
        mime = unicode(item.data(0, MIME_ROLE).toString())
        name = unicode(item.data(0, NAME_ROLE).toString())
        syntax = {'text': 'html', 'styles': 'css'}.get(category, None)
        self.edit_file.emit(name, syntax, mime)

    def request_edit(self, name):
        item = self.item_from_name(name)
        if item is not None:
            self._request_edit(item)
        else:
            error_dialog(self,
                         _('Cannot edit'),
                         _('No item with the name: %s was found') % name,
                         show=True)

    @property
    def all_files(self):
        return (category.child(i) for category in self.categories.itervalues()
                for i in xrange(category.childCount()))

    @property
    def searchable_names(self):
        ans = {
            'text': OrderedDict(),
            'styles': OrderedDict(),
            'selected': OrderedDict()
        }
        for item in self.all_files:
            category = unicode(item.data(0, CATEGORY_ROLE).toString())
            mime = unicode(item.data(0, MIME_ROLE).toString())
            name = unicode(item.data(0, NAME_ROLE).toString())
            ok = category in {'text', 'styles'}
            if ok:
                ans[category][name] = syntax_from_mime(name, mime)
            if not ok and category == 'misc':
                ok = mime in {
                    guess_type('a.' + x)
                    for x in ('opf', 'ncx', 'txt', 'xml')
                }
            if ok and item.isSelected():
                ans['selected'][name] = syntax_from_mime(name, mime)
        return ans

    def export(self, name):
        path = choose_save_file(self,
                                'tweak_book_export_file',
                                _('Choose location'),
                                filters=[(_('Files'),
                                          [name.rpartition('.')[-1].lower()])],
                                all_files=False,
                                initial_filename=name.split('/')[-1])
        if path:
            self.export_requested.emit(name, path)

    def replace(self, name):
        c = current_container()
        mt = c.mime_map[name]
        oext = name.rpartition('.')[-1].lower()
        filters = [oext]
        fname = _('Files')
        if mt in OEB_DOCS:
            fname = _('HTML Files')
            filters = 'html htm xhtm xhtml shtml'.split()
        elif is_raster_image(mt):
            fname = _('Images')
            filters = 'jpeg jpg gif png'.split()
        path = choose_files(self,
                            'tweak_book_import_file',
                            _('Choose file'),
                            filters=[(fname, filters)],
                            select_only_single_file=True)
        if not path:
            return
        path = path[0]
        ext = path.rpartition('.')[-1].lower()
        force_mt = None
        if mt in OEB_DOCS:
            force_mt = c.guess_type('a.html')
        nname = os.path.basename(path)
        nname, ext = nname.rpartition('.')[0::2]
        nname = nname + '.' + ext.lower()
        self.replace_requested.emit(name, path, nname, force_mt)

    def link_stylesheets(self, names):
        s = self.categories['styles']
        sheets = [
            unicode(s.child(i).data(0, NAME_ROLE).toString())
            for i in xrange(s.childCount())
        ]
        if not sheets:
            return error_dialog(
                self,
                _('No stylesheets'),
                _('This book currently has no stylesheets. You must first create a stylesheet'
                  ' before linking it.'),
                show=True)
        d = QDialog(self)
        d.l = l = QVBoxLayout(d)
        d.setLayout(l)
        d.setWindowTitle(_('Choose stylesheets'))
        d.la = la = QLabel(
            _('Choose the stylesheets to link. Drag and drop to re-arrange'))

        la.setWordWrap(True)
        l.addWidget(la)
        d.s = s = QListWidget(d)
        l.addWidget(s)
        s.setDragEnabled(True)
        s.setDropIndicatorShown(True)
        s.setDragDropMode(self.InternalMove)
        s.setAutoScroll(True)
        s.setDefaultDropAction(Qt.MoveAction)
        for name in sheets:
            i = QListWidgetItem(name, s)
            flags = Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable
            i.setFlags(flags)
            i.setCheckState(Qt.Checked)
        d.r = r = QCheckBox(_('Remove existing links to stylesheets'))
        r.setChecked(tprefs['remove_existing_links_when_linking_sheets'])
        l.addWidget(r)
        d.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        bb.accepted.connect(d.accept), bb.rejected.connect(d.reject)
        l.addWidget(bb)
        if d.exec_() == d.Accepted:
            tprefs['remove_existing_links_when_linking_sheets'] = r.isChecked()
            sheets = [
                unicode(s.item(il).text()) for il in xrange(s.count())
                if s.item(il).checkState() == Qt.Checked
            ]
            if sheets:
                self.link_stylesheets_requested.emit(names, sheets,
                                                     r.isChecked())
Exemple #28
0
class DiffView(QWidget):  # {{{

    SYNC_POSITION = 0.4
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, parent=None, show_open_in_editor=False):
        QWidget.__init__(self, parent)
        self.changes = [[], [], []]
        self.delta = 0
        self.l = l = QHBoxLayout(self)
        self.setLayout(l)
        self.syncpos = 0
        l.setMargin(0), l.setSpacing(0)
        self.view = DiffSplit(self, show_open_in_editor=show_open_in_editor)
        l.addWidget(self.view)
        self.add_diff = self.view.add_diff
        self.scrollbar = QScrollBar(self)
        l.addWidget(self.scrollbar)
        self.syncing = False
        self.bars = []
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.resize_timer.timeout.connect(self.resize_debounced)
        for i, bar in enumerate(
            (self.scrollbar, self.view.left.verticalScrollBar(),
             self.view.right.verticalScrollBar())):
            self.bars.append(bar)
            bar.valueChanged[int].connect(partial(self.scrolled, i))
        self.view.left.resized.connect(self.resized)
        for i, v in enumerate(
            (self.view.left, self.view.right, self.view.handle(1))):
            v.wheel_event.connect(self.scrollbar.wheelEvent)
            if i < 2:
                v.next_change.connect(self.next_change)
                v.line_activated.connect(self.line_activated)
                v.scrolled.connect(partial(self.scrolled, i + 1))

    def next_change(self, delta):
        assert delta in (1, -1)
        position = self.get_position_from_scrollbar(0)
        if position[0] == 'in':
            p = n = position[1]
        else:
            p, n = position[1], position[1] + 1
            if p < 0:
                p = None
            if n >= len(self.changes[0]):
                n = None
        if p == n:
            nc = p + delta
            if nc < 0 or nc >= len(self.changes[0]):
                nc = None
        else:
            nc = {1: n, -1: p}[delta]
        if nc is None:
            self.scrollbar.setValue(0 if delta ==
                                    -1 else self.scrollbar.maximum())
        else:
            val = self.scrollbar.value()
            self.scroll_to(0, ('in', nc, 0))
            nval = self.scrollbar.value()
            if nval == val:
                nval += 5 * delta
                if 0 <= nval <= self.scrollbar.maximum():
                    self.scrollbar.setValue(nval)

    def resized(self):
        self.resize_timer.start(300)

    def resize_debounced(self):
        self.view.resized()
        self.calculate_length()
        self.adjust_range()
        self.view.handle(1).update()

    def get_position_from_scrollbar(self, which):
        changes = self.changes[which]
        bar = self.bars[which]
        syncpos = self.syncpos + bar.value()
        prev = 0
        for i, (top, bot, kind) in enumerate(changes):
            if syncpos <= bot:
                if top <= syncpos:
                    # syncpos is inside a change
                    try:
                        ratio = float(syncpos - top) / (bot - top)
                    except ZeroDivisionError:
                        ratio = 0
                    return 'in', i, ratio
                else:
                    # syncpos is after the previous change
                    offset = syncpos - prev
                    return 'after', i - 1, offset
            else:
                # syncpos is after the current change
                prev = bot
        offset = syncpos - prev
        return 'after', len(changes) - 1, offset

    def scroll_to(self, which, position):
        changes = self.changes[which]
        bar = self.bars[which]
        val = None
        if position[0] == 'in':
            change_idx, ratio = position[1:]
            start, end = changes[change_idx][:2]
            val = start + int((end - start) * ratio)
        else:
            change_idx, offset = position[1:]
            start = 0 if change_idx < 0 else changes[change_idx][1]
            val = start + offset
        bar.setValue(val - self.syncpos)

    def scrolled(self, which, *args):
        if self.syncing:
            return
        position = self.get_position_from_scrollbar(which)
        with self:
            for x in {0, 1, 2} - {which}:
                self.scroll_to(x, position)
        self.view.handle(1).update()

    def __enter__(self):
        self.syncing = True

    def __exit__(self, *args):
        self.syncing = False

    def clear(self):
        with self:
            self.view.clear()
            self.changes = [[], [], []]
            self.delta = 0
            self.scrollbar.setRange(0, 0)

    def adjust_range(self):
        ls, rs = self.view.left.verticalScrollBar(
        ), self.view.right.verticalScrollBar()
        self.scrollbar.setPageStep(min(ls.pageStep(), rs.pageStep()))
        self.scrollbar.setSingleStep(min(ls.singleStep(), rs.singleStep()))
        self.scrollbar.setRange(0, ls.maximum() + self.delta)
        self.scrollbar.setVisible(
            self.view.left.blockCount() > ls.pageStep()
            or self.view.right.blockCount() > rs.pageStep())
        self.syncpos = int(ceil(self.scrollbar.pageStep() *
                                self.SYNC_POSITION))

    def finalize(self):
        self.view.finalize()
        self.changes = [[], [], []]
        self.calculate_length()
        self.adjust_range()

    def calculate_length(self):
        delta = 0
        line_number_changes = ([], [])
        for v, lmap, changes in zip((self.view.left, self.view.right),
                                    ({}, {}), line_number_changes):
            b = v.document().firstBlock()
            ebl = v.document().documentLayout().ensureBlockLayout
            last_line_count = 0
            while b.isValid():
                ebl(b)
                lmap[b.blockNumber()] = last_line_count
                last_line_count += b.layout().lineCount()
                b = b.next()
            for top, bot, kind in v.changes:
                changes.append((lmap[top], lmap[bot], kind))

        changes = []
        for (l_top, l_bot, kind), (r_top, r_bot,
                                   kind) in zip(*line_number_changes):
            height = max(l_bot - l_top, r_bot - r_top)
            top = delta + l_top
            changes.append((top, top + height, kind))
            delta = top + height - l_bot
        self.changes, self.delta = (changes, ) + line_number_changes, delta

    def handle_key(self, ev):
        amount, d = None, 1
        key = ev.key()
        if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_J, Qt.Key_K):
            amount = self.scrollbar.singleStep()
            if key in (Qt.Key_Up, Qt.Key_K):
                d = -1
        elif key in (Qt.Key_PageUp, Qt.Key_PageDown):
            amount = self.scrollbar.pageStep()
            if key in (Qt.Key_PageUp, ):
                d = -1
        elif key in (Qt.Key_Home, Qt.Key_End):
            self.scrollbar.setValue(0 if key ==
                                    Qt.Key_Home else self.scrollbar.maximum())
            return True
        elif key in (Qt.Key_N, Qt.Key_P):
            self.next_change(1 if key == Qt.Key_N else -1)
            return True

        if amount is not None:
            self.scrollbar.setValue(self.scrollbar.value() + d * amount)
            return True
        return False
Exemple #29
0
class TOCEditor(QDialog):

    explode_done = pyqtSignal(object)
    writing_done = pyqtSignal(object)

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

        t = title or current_container().mi.title
        self.book_title = t
        self.setWindowTitle(_('Edit the ToC in %s') % t)
        self.setWindowIcon(QIcon(I('toc.png')))

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

        self.stacks = s = QStackedWidget(self)
        l.addWidget(s)
        self.toc_view = TOCView(self)
        self.toc_view.add_new_item.connect(self.add_new_item)
        s.addWidget(self.toc_view)
        self.item_edit = ItemEdit(self)
        s.addWidget(self.item_edit)

        bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok
                                        | QDialogButtonBox.Cancel)
        l.addWidget(bb)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)

        self.read_toc()

        self.resize(950, 630)
        geom = gprefs.get('toc_editor_window_geom', None)
        if geom is not None:
            self.restoreGeometry(bytes(geom))

    def add_new_item(self, item, where):
        self.item_edit(item, where)
        self.stacks.setCurrentIndex(1)

    def accept(self):
        if self.stacks.currentIndex() == 1:
            self.toc_view.update_item(*self.item_edit.result)
            gprefs['toc_edit_splitter_state'] = bytearray(
                self.item_edit.splitter.saveState())
            self.stacks.setCurrentIndex(0)
        elif self.stacks.currentIndex() == 0:
            self.write_toc()
            super(TOCEditor, self).accept()

    def really_accept(self, tb):
        gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
        if tb:
            error_dialog(self,
                         _('Failed to write book'),
                         _('Could not write %s. Click "Show details" for'
                           ' more information.') % self.book_title,
                         det_msg=tb,
                         show=True)
            gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
            super(TOCEditor, self).reject()
            return

        super(TOCEditor, self).accept()

    def reject(self):
        if not self.bb.isEnabled():
            return
        if self.stacks.currentIndex() == 1:
            gprefs['toc_edit_splitter_state'] = bytearray(
                self.item_edit.splitter.saveState())
            self.stacks.setCurrentIndex(0)
        else:
            gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry())
            super(TOCEditor, self).reject()

    def read_toc(self):
        self.toc_view(current_container())
        self.item_edit.load(current_container())
        self.stacks.setCurrentIndex(0)

    def write_toc(self):
        toc = self.toc_view.create_toc()
        commit_toc(current_container(),
                   toc,
                   lang=self.toc_view.toc_lang,
                   uid=self.toc_view.toc_uid)
Exemple #30
0
class DiffSplitHandle(QSplitterHandle):  # {{{

    WIDTH = 30  # px
    wheel_event = pyqtSignal(object)

    def event(self, ev):
        if ev.type() in (ev.HoverEnter, ev.HoverLeave):
            self.hover = ev.type() == ev.HoverEnter
        return QSplitterHandle.event(self, ev)

    def paintEvent(self, event):
        QSplitterHandle.paintEvent(self, event)
        left, right = self.parent().left, self.parent().right
        painter = QPainter(self)
        painter.setClipRect(event.rect())
        w = self.width()
        h = self.height()
        painter.setRenderHints(QPainter.Antialiasing, True)

        C = 16  # Curve factor.

        def create_line(ly, ry, right_to_left=False):
            ' Create path that represents upper or lower line of change marker '
            line = QPainterPath()
            if not right_to_left:
                line.moveTo(0, ly)
                line.cubicTo(C, ly, w - C, ry, w, ry)
            else:
                line.moveTo(w, ry)
                line.cubicTo(w - C, ry, C, ly, 0, ly)
            return line

        ldoc, rdoc = left.document(), right.document()
        lorigin, rorigin = left.contentOffset(), right.contentOffset()
        lfv, rfv = left.firstVisibleBlock().blockNumber(
        ), right.firstVisibleBlock().blockNumber()
        lines = []

        for (ltop, lbot, kind), (rtop, rbot,
                                 kind) in zip(left.changes, right.changes):
            if lbot < lfv and rbot < rfv:
                continue
            ly_top = left.blockBoundingGeometry(
                ldoc.findBlockByNumber(ltop)).translated(lorigin).y()
            ly_bot = left.blockBoundingGeometry(
                ldoc.findBlockByNumber(lbot)).translated(lorigin).y()
            ry_top = right.blockBoundingGeometry(
                rdoc.findBlockByNumber(rtop)).translated(rorigin).y()
            ry_bot = right.blockBoundingGeometry(
                rdoc.findBlockByNumber(rbot)).translated(rorigin).y()
            if max(ly_top, ly_bot, ry_top, ry_bot) < 0:
                continue
            if min(ly_top, ly_bot, ry_top, ry_bot) > h:
                break

            upper_line = create_line(ly_top, ry_top)
            lower_line = create_line(ly_bot, ry_bot, True)

            region = QPainterPath()
            region.moveTo(0, ly_top)
            region.connectPath(upper_line)
            region.lineTo(w, ry_bot)
            region.connectPath(lower_line)
            region.closeSubpath()

            painter.fillPath(region, left.diff_backgrounds[kind])
            for path, aa in zip((upper_line, lower_line),
                                (ly_top != ry_top, ly_bot != ry_bot)):
                lines.append((kind, path, aa))

        for kind, path, aa in sorted(
                lines, key=lambda x: {'replace': 0}.get(x[0], 1)):
            painter.setPen(left.diff_foregrounds[kind])
            painter.setRenderHints(QPainter.Antialiasing, aa)
            painter.drawPath(path)

        painter.setFont(left.heading_font)
        for (lnum, text), (rnum, text) in zip(left.headers, right.headers):
            ltop, lbot, rtop, rbot = lnum, lnum + 3, rnum, rnum + 3
            if lbot < lfv and rbot < rfv:
                continue
            ly_top = left.blockBoundingGeometry(
                ldoc.findBlockByNumber(ltop)).translated(lorigin).y()
            ly_bot = left.blockBoundingGeometry(
                ldoc.findBlockByNumber(lbot)).translated(lorigin).y()
            ry_top = right.blockBoundingGeometry(
                rdoc.findBlockByNumber(rtop)).translated(rorigin).y()
            ry_bot = right.blockBoundingGeometry(
                rdoc.findBlockByNumber(rbot)).translated(rorigin).y()
            if max(ly_top, ly_bot, ry_top, ry_bot) < 0:
                continue
            if min(ly_top, ly_bot, ry_top, ry_bot) > h:
                break
            ly = painter.boundingRect(3, ly_top, left.width(),
                                      ly_bot - ly_top - 5, Qt.TextSingleLine,
                                      text).bottom() + 3
            ry = painter.boundingRect(3, ry_top, right.width(),
                                      ry_bot - ry_top - 5, Qt.TextSingleLine,
                                      text).bottom() + 3
            line = create_line(ly, ry)
            painter.setPen(QPen(left.palette().text(), 2))
            painter.setRenderHints(QPainter.Antialiasing, ly != ry)
            painter.drawPath(line)

        painter.end()
        # Paint the splitter without the change lines if the mouse is over the
        # splitter
        if getattr(self, 'hover', False):
            QSplitterHandle.paintEvent(self, event)

    def sizeHint(self):
        ans = QSplitterHandle.sizeHint(self)
        ans.setWidth(self.WIDTH)
        return ans

    def wheelEvent(self, ev):
        if ev.orientation() == Qt.Vertical:
            self.wheel_event.emit(ev)
        else:
            return QSplitterHandle.wheelEvent(self, ev)