class FileController(object): def __init__(self, manager, opusDataPath, parentWidget, listen_to_menu=True): self.addTree(opusDataPath, parentWidget) self.currentColumn = None self.currentIndex = None self.classification = "" self.manager = manager if listen_to_menu: QObject.connect( self.treeview, SIGNAL("customContextMenuRequested(const QPoint &)"), self.process_custom_menu) def addTree(self, opusDataPath, parentWidget): self.containerWidget = parentWidget self.opusDataPath = opusDataPath self.treeview = QTreeView() filters = QStringList() filters.append("*.*") #filters.append("*.py") #filters.append("*.shp") #filters.append("*.tif") self.model = QDirModel(filters, QDir.Files | QDir.AllDirs | QDir.NoDotAndDotDot, QDir.Name) self.treeview.setModel(self.model) if self.opusDataPath: self.treeview.setRootIndex(self.model.index(self.opusDataPath)) self.treeview.setColumnWidth(0, 200) self.treeview.hideColumn(2) self.treeview.hideColumn(3) self.containerWidget.layout().addWidget(self.treeview) # Hook up to the mousePressEvent and pressed self.treeview.setContextMenuPolicy(Qt.CustomContextMenu) def close(self): self.treeview.hide() self.containerWidget.layout().removeWidget(self.treeview) return True def process_custom_menu(self, position): raise Exception('Method processCustomMenu is not implemented')
class FileController(object): def __init__(self, manager, opusDataPath, parentWidget, listen_to_menu = True): self.addTree(opusDataPath, parentWidget) self.currentColumn = None self.currentIndex = None self.classification = "" self.manager = manager if listen_to_menu: QObject.connect(self.treeview, SIGNAL("customContextMenuRequested(const QPoint &)"), self.process_custom_menu) def addTree(self, opusDataPath, parentWidget): self.containerWidget = parentWidget self.opusDataPath = opusDataPath self.treeview = QTreeView() filters = QStringList() filters.append("*.*") #filters.append("*.py") #filters.append("*.shp") #filters.append("*.tif") self.model = QDirModel(filters, QDir.Files|QDir.AllDirs|QDir.NoDotAndDotDot, QDir.Name) self.treeview.setModel(self.model) if self.opusDataPath: self.treeview.setRootIndex(self.model.index(self.opusDataPath)) self.treeview.setColumnWidth(0,200) self.treeview.hideColumn(2) self.treeview.hideColumn(3) self.containerWidget.layout().addWidget(self.treeview) # Hook up to the mousePressEvent and pressed self.treeview.setContextMenuPolicy(Qt.CustomContextMenu) def close(self): self.treeview.hide() self.containerWidget.layout().removeWidget(self.treeview) return True def process_custom_menu(self, position): raise Exception('Method processCustomMenu is not implemented')
class XNavigationEdit(XLineEdit): """ """ navigationChanged = Signal() __designer_icon__ = projexui.resources.find('img/ui/navigate.png') def __init__( self, parent = None ): super(XNavigationEdit, self).__init__( parent ) # define custom properties self._separator = '/' self._partsEditingEnabled = True self._originalText = '' self._scrollWidget = QScrollArea(self) self._partsWidget = QWidget(self._scrollWidget) self._buttonGroup = QButtonGroup(self) self._scrollAmount = 0 self._navigationModel = None # create the completer tree palette = self.palette() palette.setColor(palette.Base, palette.color(palette.Window)) palette.setColor(palette.Text, palette.color(palette.WindowText)) bg = palette.color(palette.Highlight) abg = bg.darker(115) fg = palette.color(palette.HighlightedText) sbg = 'rgb(%s, %s, %s)' % (bg.red(), bg.green(), bg.blue()) sabg = 'rgb(%s, %s, %s)' % (abg.red(), abg.green(), abg.blue()) sfg = 'rgb(%s, %s, %s)' % (fg.red(), fg.green(), fg.blue()) style = 'QTreeView::item:hover { '\ ' color: %s;'\ ' background: qlineargradient(x1:0,'\ ' y1:0,'\ ' x2:0,'\ ' y2:1,'\ ' stop: 0 %s,'\ ' stop: 1 %s);'\ '}' % (sfg, sbg, sabg) self._completerTree = QTreeView(self) self._completerTree.setStyleSheet(style) self._completerTree.header().hide() self._completerTree.setFrameShape(QTreeView.Box) self._completerTree.setFrameShadow(QTreeView.Plain) self._completerTree.setPalette(palette) self._completerTree.setEditTriggers(QTreeView.NoEditTriggers) self._completerTree.setWindowFlags(Qt.Popup) self._completerTree.installEventFilter(self) self._completerTree.setRootIsDecorated(False) self._completerTree.setItemsExpandable(False) # create the editing widget layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addStretch() self._scrollWidget.setFrameShape( QScrollArea.NoFrame ) self._scrollWidget.setFocusPolicy(Qt.NoFocus) self._scrollWidget.setWidget(self._partsWidget) self._scrollWidget.setWidgetResizable(True) self._scrollWidget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._scrollWidget.setAlignment(Qt.AlignTop | Qt.AlignRight) self._scrollWidget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._scrollWidget.setContentsMargins(0, 0, 0, 0) self._scrollWidget.setViewportMargins(0, 0, 0, 0) self._scrollWidget.move(2, 2) self._partsWidget.setLayout(layout) self._partsWidget.setCursor(Qt.ArrowCursor) self._partsWidget.setAutoFillBackground(True) self._partsWidget.setFixedHeight(self.height() - 12) palette = self._partsWidget.palette() palette.setColor(palette.Background, palette.color(palette.Base)) self._partsWidget.setPalette(palette) # create connections self._completerTree.clicked.connect( self.navigateToIndex ) self._buttonGroup.buttonClicked.connect( self.handleButtonClick ) self._scrollWidget.horizontalScrollBar().valueChanged.connect( self.scrollParts ) def acceptEdit( self ): """ Accepts the current text and rebuilds the parts widget. """ if ( self._partsWidget.isVisible() ): return False use_completion = self.completer().popup().isVisible() completion = self.completer().currentCompletion() self._completerTree.hide() self.completer().popup().hide() if ( use_completion ): self.setText(completion) else: self.rebuild() return True def cancelEdit( self ): """ Rejects the current edit and shows the parts widget. """ if ( self._partsWidget.isVisible() ): return False self._completerTree.hide() self.completer().popup().hide() self.setText(self._originalText) return True def currentItem( self ): """ Returns the current navigation item from the current path. :return <XNavigationItem> || None """ model = self.navigationModel() if ( not model ): return None return model.itemByPath(self.text()) def eventFilter( self, object, event ): """ Filters the events for the inputed object through this edit. :param object | <QObject> event | <QEvent> :return <bool> | consumed """ if ( event.type() == event.KeyPress ): if ( event.key() == Qt.Key_Escape ): self._completerTree.hide() self.completer().popup().hide() self.cancelEdit() elif ( event.key() in (Qt.Key_Return, Qt.Key_Enter) ): self.acceptEdit() return True elif ( event.key() == Qt.Key_Tab ): if ( self.completer().popup().isVisible() ): text = str(self.completer().currentCompletion()) super(XNavigationEdit, self).setText(text) return True else: self.acceptEdit() return False elif ( event.type() == event.MouseButtonPress ): if ( not self._completerTree.rect().contains(event.pos()) ): self._completerTree.hide() self.completer().popup().hide() self.cancelEdit() return False def focusOutEvent( self, event ): """ Overloads the focus out event to cancel editing when the widget loses focus. :param event | <QFocusEvent> """ super(XNavigationEdit, self).focusOutEvent(event) self.cancelEdit() def handleButtonClick( self, button ): """ Handle the event when a user clicks on one of the part buttons. :param button | <QToolButton> """ path = button.property('path') is_completer = button.property('is_completer') # popup a completion menu if ( unwrapVariant(is_completer) ): model = self.navigationModel() if ( not model ): return sep = self.separator() path = str(unwrapVariant(path)) item = model.itemByPath(path, includeRoot = True) if ( not item ): return curr_path = str(self.text()).strip(self.separator()) curr_path = curr_path.replace(path, '').strip(self.separator()) child_name = '' if ( curr_path ): child_name = curr_path.split(self.separator())[0] index = model.indexFromItem(item) self._completerTree.move(QCursor.pos()) self._completerTree.setRootIndex(index) self._completerTree.verticalScrollBar().setValue(0) if ( child_name ): child_item = None for i in range(item.rowCount()): child = item.child(i) if ( child.text() == child_name ): child_item = child break if ( child_item ): child_index = model.indexFromItem(child_item) self._completerTree.setCurrentIndex(child_index) self._completerTree.scrollTo(child_index) self._completerTree.show() self._completerTree.setUpdatesEnabled(True) else: self.setText(unwrapVariant(path)) def keyPressEvent( self, event ): """ Overloads the key press event to listen for escape calls to cancel the parts editing. :param event | <QKeyPressEvent> """ if ( self.scrollWidget().isHidden() ): if ( event.key() == Qt.Key_Escape ): self.cancelEdit() return elif ( event.key() in (Qt.Key_Return, Qt.Key_Enter) ): self.acceptEdit() return elif ( event.key() == Qt.Key_A and event.modifiers() == Qt.ControlModifier ): self.startEdit() super(XNavigationEdit, self).keyPressEvent(event) def mouseDoubleClickEvent( self, event ): """ Overloads the system to enable editing when a user double clicks. :param event | <QMouseEvent> """ super(XNavigationEdit, self).mouseDoubleClickEvent(event) self.startEdit() def navigationModel( self ): """ Returns the navigation model linked with this edit. :return <XNavigationModel> || None """ return self._navigationModel def navigateToIndex( self, index ): """ Navigates to the inputed action's path. :param action | <QAction> """ self._completerTree.hide() item = self._navigationModel.itemFromIndex(index) self.setText(self._navigationModel.itemPath(item)) def parts( self ): """ Returns the parts that are used for this system. :return [<str>, ..] """ path = str(self.text()).strip(self.separator()) if ( not path ): return [] return path.split(self.separator()) def partsWidget( self ): """ Returns the widget that contains the parts system. :return <QScrollArea> """ return self._partsWidget def startEdit( self ): """ Rebuilds the pathing based on the parts. """ self._originalText = self.text() self.scrollWidget().hide() self.setFocus() self.selectAll() def rebuild( self ): """ Rebuilds the parts widget with the latest text. """ navitem = self.currentItem() if ( navitem ): navitem.initialize() self.setUpdatesEnabled(False) self.scrollWidget().show() self._originalText = '' partsw = self.partsWidget() for button in self._buttonGroup.buttons(): self._buttonGroup.removeButton(button) button.close() button.setParent(None) button.deleteLater() # create the root button layout = partsw.layout() parts = self.parts() button = QToolButton(partsw) button.setAutoRaise(True) button.setMaximumWidth(12) button.setArrowType(Qt.RightArrow) button.setProperty('path', wrapVariant('')) button.setProperty('is_completer', wrapVariant(True)) last_button = button self._buttonGroup.addButton(button) layout.insertWidget(0, button) # check to see if we have a navigation model setup if ( self._navigationModel ): last_item = self._navigationModel.itemByPath(self.text()) show_last = last_item and last_item.rowCount() > 0 else: show_last = False # load the navigation system count = len(parts) for i, part in enumerate(parts): path = self.separator().join(parts[:i+1]) button = QToolButton(partsw) button.setAutoRaise(True) button.setText(part) if ( self._navigationModel ): item = self._navigationModel.itemByPath(path) if ( item ): button.setIcon(item.icon()) button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setProperty('path', wrapVariant(path)) button.setProperty('is_completer', wrapVariant(False)) self._buttonGroup.addButton(button) layout.insertWidget((i * 2) + 1, button) # determine if we should show the final button if ( show_last or i < (count - 1) ): button = QToolButton(partsw) button.setAutoRaise(True) button.setMaximumWidth(12) button.setArrowType(Qt.RightArrow) button.setProperty('path', wrapVariant(path)) button.setProperty('is_completer', wrapVariant(True)) self._buttonGroup.addButton(button) layout.insertWidget((i * 2) + 2, button) last_button = button if ( self.scrollWidget().width() < partsw.width() ): self.scrollParts(partsw.width() - self.scrollWidget().width()) self.setUpdatesEnabled(True) self.navigationChanged.emit() def resizeEvent( self, event ): """ Resizes the current widget and its parts widget. :param event | <QResizeEvent> """ super(XNavigationEdit, self).resizeEvent(event) w = self.width() h = self.height() self._scrollWidget.resize(w - 4, h - 4) if ( self._scrollWidget.width() < self._partsWidget.width() ): self.scrollParts( self._partsWidget.width() - self._scrollWidget.width() ) def scrollParts( self, amount ): """ Scrolls the parts to offset the scrolling amount. :param amount | <int> """ change = self._scrollAmount - amount self._partsWidget.scroll(change, 0) self._scrollAmount = amount def scrollWidget( self ): """ Returns the scrolling widget. :return <QScrollArea> """ return self._scrollWidget def separator( self ): """ Returns the separation character that is used for this edit. :return <str> """ return self._separator def setTopLevelItems( self, items ): """ Initializes the navigation system to start with the inputed root \ item. :param item | <XNavigationItem> """ if ( not self._navigationModel ): self.setNavigationModel(XNavigationModel(self)) self._navigationModel.setTopLevelItems(items) def setNavigationModel( self, model ): """ Sets the navigation model for this edit. :param model | <XNavigationModel> """ self._navigationModel = model self._completerTree.setModel(model) if ( model ): model.setSeparator(self.separator()) completer = XNavigationCompleter(model, self) self.setCompleter(completer) completer.popup().installEventFilter(self) else: self.setCompleter(None) self.rebuild() def setParts( self, parts ): """ Sets the path for this edit widget by providing the parts to the path. :param parts | [<str>, ..] """ self.setText(self.separator().join(map(str, parts))) def setSeparator( self, separator ): """ Sets the separator to the inputed character. :param separator | <str> """ self._separator = separator if ( self._navigationModel ): self._navigationModel.setSeparator(separator) self.rebuild() def setText( self, text ): """ Sets the text for this edit to the inputed text. :param text | <str> """ super(XNavigationEdit, self).setText(text) self.scrollWidget().show() if ( text == '' or self._originalText != text ): self.rebuild()
class Lectern(QMainWindow): def __init__(self, parent=None): super(Lectern, self).__init__(parent) self.anchor = None self.initMainMenu() self.initToolbar() splitter = QSplitter() self.tocView = QTreeView() self.tocView.clicked.connect(self.navTo) self.tocModel = TableOfContents() self.tocModel.isEmpty.connect(self.handleTOCLoad) self.tocView.setModel(self.tocModel) self.tocView.expandAll() self.tocView.hide() splitter.addWidget(self.tocView) self.webView = QGraphicsWebView() frame = self.webView.page().mainFrame() scene = QGraphicsScene() scene.addItem(self.webView) self.graphicsView = GraphicsView(scene) self.graphicsView.setFrameShape(QFrame.NoFrame) glWidget = QGLWidget(self) self.graphicsView.setViewport(glWidget) self.webView.loadFinished.connect(self.handleLoad) splitter.addWidget(self.graphicsView) self.setCentralWidget(splitter) self.ebook_info = {} self.setWindowTitle('Lectern') try: self.ebook_info = self.openBook(QApplication.arguments()[1]) except IndexError: pass def initMainMenu(self): menuBar = self.menuBar() menuBar.setNativeMenuBar(True) # TODO: add CROSS-PLATFORM shortcut keys. (e.g. For Quit, use ⌘Q on Mac OS X, ALT-F4 elsewhere) fileMenu = QMenu('File', menuBar) navMenu = QMenu('Navigate', menuBar) # File Menu openAction = QAction('Open', fileMenu) openAction.triggered.connect(self.chooseEbook) fileMenu.addAction(openAction) quitAction = QAction('Quit', fileMenu) quitAction.triggered.connect(self.closeEvent) fileMenu.addAction(quitAction) # Nav Menu prevChatperAction = QAction('Previous Chapter', navMenu) prevChatperAction.triggered.connect(self.prevChapter) navMenu.addAction(prevChatperAction) nextChatperAction = QAction('Next Chapter', navMenu) nextChatperAction.triggered.connect(self.nextChapter) navMenu.addAction(nextChatperAction) menuBar.addMenu(fileMenu) menuBar.addMenu(navMenu) def initToolbar(self): toolBar = QToolBar(self) chooseAction = QAction(self.style().standardIcon( QStyle.SP_DialogOpenButton), 'Open', toolBar) chooseAction.triggered.connect(self.chooseEbook) toolBar.addAction(chooseAction) self.prevAction = QAction(self.style().standardIcon( QStyle.SP_ArrowBack), 'Go back', toolBar) self.prevAction.setEnabled(False) self.prevAction.triggered.connect(self.prevChapter) toolBar.addAction(self.prevAction) self.nextAction = QAction(self.style().standardIcon( QStyle.SP_ArrowForward), 'Go forward', toolBar) self.nextAction.setEnabled(False) self.nextAction.triggered.connect(self.nextChapter) toolBar.addAction(self.nextAction) self.addToolBar(toolBar) def chooseEbook(self): path = QFileDialog.getOpenFileName(self, 'Open eBook', QDesktopServices.storageLocation( QDesktopServices.DocumentsLocation),'EPUBs (*.epub)') if not isfile(path): return if self.ebook_info is not None and 'temp_path' in self.ebook_info: if exists(self.ebook_info['temp_path']): rmtree(self.ebook_info['temp_path']) path = QDir.toNativeSeparators(path) self.ebook_info = self.openBook(path) def openBook(self, path): ebook_info = {} path = realpath(path) if not isfile(path): QMessageBox.critical(self, 'File not found', 'File not found') mimetype, _ = guess_type(path) if mimetype != 'application/epub+zip': QMessageBox.critical(self, 'Not an EPUB', 'Not an EPUB') return None ebook = ZipFile(path) names = ebook.namelist() if not 'META-INF/container.xml' in names: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'container.xml not '\ 'found') return None container_tree = etree.parse(ebook.open('META-INF/container.xml')) rootfile = container_tree.xpath("//*[local-name() = 'rootfile']") if len(rootfile) == 0: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'root not found in '\ 'manifest') return None content_opf = rootfile[0].get('full-path') if content_opf is None: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'content.opf not found') return None ebook_info['opf_root'] = posixpath.dirname(content_opf) tree = etree.parse(ebook.open(content_opf)) manifest = tree.xpath("*[local-name() = 'manifest']") if len(manifest) == 0: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Manifest not found') return None manifest = manifest[0] items = {} for item in manifest: item_id = item.get('id') if item_id is None: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Item has no id') return None href = item.get('href') if href is None: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Item has no href') return None items[item_id] = href spine = tree.xpath("*[local-name() = 'spine']") if len(spine) == 0: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Spine not found') return None spine = spine[0] ebook_info['chapters'] = [] for itemref in spine: idref = itemref.get('idref') if not idref in items: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Item in spine '\ 'not found in manifest') return None ebook_info['chapters'].append(items[idref]) if len(ebook_info['chapters']) == 0: ebook.close() QMessageBox.critical(self, 'Invalid EPUB', 'Content not found') return None # Table of contents toc = tree.find("//*[@href='toc.ncx']") if toc is not None: toc_path = posixpath.join(ebook_info['opf_root'], 'toc.ncx') if toc_path in names: toc_tree = etree.parse(ebook.open(toc_path)) navMap = toc_tree.xpath("//*[local-name() = 'navMap']") if len(navMap) > 0: self.tocModel.importNavMap(navMap[0]) temp = QDir.toNativeSeparators(QDesktopServices.storageLocation( QDesktopServices.TempLocation)) # In case we have two copies of Lectern opening the same book. filename = '{0}-{1}'.format(splitext(basename(path))[0], uuid4()) ebook_info['temp_path'] = join(temp, filename) if exists(ebook_info['temp_path']): rmtree(ebook_info['temp_path']) ebook.extractall(ebook_info['temp_path']) ebook.close() ebook_info['index'] = 0 url = join(ebook_info['temp_path'], ebook_info['opf_root'], ebook_info['chapters'][0]) self.webView.setUrl(QUrl(url)) if len(ebook_info['chapters']) > 1: self.nextAction.setEnabled(True) return ebook_info def prevChapter(self): index = self.ebook_info['index'] chapters = self.ebook_info['chapters'] if index > 0: index -= 1 if index == 0: self.prevAction.setEnabled(False) url = join(self.ebook_info['temp_path'], self.ebook_info['opf_root'], chapters[index]) self.webView.setUrl(QUrl(url)) self.ebook_info['index'] = index self.nextAction.setEnabled(True) def nextChapter(self): index = self.ebook_info['index'] chapters = self.ebook_info['chapters'] if index < len(chapters) - 1: index += 1 if index == len(chapters) - 1: self.nextAction.setEnabled(False) url = join(self.ebook_info['temp_path'], self.ebook_info['opf_root'], chapters[index]) self.webView.setUrl(QUrl(url)) self.ebook_info['index'] = index self.prevAction.setEnabled(True) def closeBook(self): if self.ebook_info is not None and 'temp_path' in self.ebook_info: if exists(self.ebook_info['temp_path']): rmtree(self.ebook_info['temp_path']) self.ebook_info = None self.tocView.hide() self.prevAction.setEnabled(False) self.nextAction.setEnabled(False) def closeEvent(self, event = 0): if(event == 0): event = PyQt4.QtGui.QCloseEvent() self.closeBook() super(Lectern, self).closeEvent(event) # Suppress "cannot make invalid context current" warnings sys.exit(0) def navTo(self, index): navPoint = index.internalPointer() href = posixpath.join(self.ebook_info['temp_path'], self.ebook_info['opf_root'], navPoint.src) try: path, anchor = href.split('#') if path == self.webView.url().path(): self.webView.page().mainFrame().scrollToAnchor(anchor) return else: self.anchor = anchor except ValueError: pass url = QUrl.fromEncoded(href) self.webView.setUrl(url) def handleLoad(self, ok): if self.anchor is not None: self.webView.page().mainFrame().addToJavaScriptWindowObject("app", self); self.webView.page().mainFrame().scrollToAnchor(self.anchor) def handleTOCLoad(self, isEmpty): if isEmpty: self.tocView.hide() else: self.tocView.show()