class MainGlyphWindow(QMainWindow): def __init__(self, glyph, parent=None): super().__init__(parent) menuBar = self.menuBar() fileMenu = QMenu("&File", self) fileMenu.addAction("E&xit", self.close, QKeySequence.Quit) menuBar.addMenu(fileMenu) editMenu = QMenu("&Edit", self) self._undoAction = editMenu.addAction("&Undo", self.undo, QKeySequence.Undo) self._redoAction = editMenu.addAction("&Redo", self.redo, QKeySequence.Redo) editMenu.addSeparator() # XXX action = editMenu.addAction("C&ut", self.cutOutlines, QKeySequence.Cut) action.setEnabled(False) self._copyAction = editMenu.addAction("&Copy", self.copyOutlines, QKeySequence.Copy) editMenu.addAction("&Paste", self.pasteOutlines, QKeySequence.Paste) editMenu.addAction("Select &All", self.selectAll, QKeySequence.SelectAll) editMenu.addAction("&Deselect", self.deselect, "Ctrl+D") menuBar.addMenu(editMenu) glyphMenu = QMenu("&Glyph", self) glyphMenu.addAction("&Next Glyph", lambda: self.glyphOffset(1), "End") glyphMenu.addAction("&Previous Glyph", lambda: self.glyphOffset(-1), "Home") glyphMenu.addAction("&Go To…", self.changeGlyph, "G") glyphMenu.addSeparator() self._layerAction = glyphMenu.addAction("&Layer Actions…", self.layerActions, "L") menuBar.addMenu(glyphMenu) # create tools and buttons toolBars self._tools = [] self._toolsActionGroup = QActionGroup(self) self._toolsToolBar = QToolBar("Tools", self) self._toolsToolBar.setMovable(False) self._buttons = [] self._buttonsToolBar = QToolBar("Buttons", self) self._buttonsToolBar.setMovable(False) self.addToolBar(self._toolsToolBar) self.addToolBar(self._buttonsToolBar) # http://www.setnode.com/blog/right-aligning-a-button-in-a-qtoolbar/ self._layersToolBar = QToolBar("Layers", self) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._currentLayerBox = QComboBox(self) self._currentLayerBox.currentIndexChanged.connect(self._layerChanged) self._layersToolBar.addWidget(spacer) self._layersToolBar.addWidget(self._currentLayerBox) self._layersToolBar.setContentsMargins(0, 0, 2, 0) self._layersToolBar.setMovable(False) self.addToolBar(self._layersToolBar) viewMenu = self.createPopupMenu() viewMenu.setTitle("View") viewMenu.addSeparator() action = viewMenu.addAction("Lock Toolbars", self.lockToolBars) action.setCheckable(True) action.setChecked(True) menuBar.addMenu(viewMenu) self.view = GlyphView(self) self.setGlyph(glyph) selectionTool = self.installTool(SelectionTool) selectionTool.trigger() self.installTool(PenTool) self.installTool(RulerTool) self.installTool(KnifeTool) self.installButton(RemoveOverlapButton) self.setCentralWidget(self.view.scrollArea()) self.resize(900, 700) self.view.setFocus(True) # ---------- # Menu items # ---------- def glyphOffset(self, offset): currentGlyph = self.view.glyph() font = currentGlyph.font glyphOrder = font.glyphOrder # should be enforced in fontView already if not (glyphOrder and len(glyphOrder)): return index = glyphOrder.index(currentGlyph.name) newIndex = (index + offset) % len(glyphOrder) glyph = font[glyphOrder[newIndex]] self.setGlyph(glyph) def changeGlyph(self): glyph = self.view.glyph() newGlyph, ok = GotoDialog.getNewGlyph(self, glyph) if ok and newGlyph is not None: self.setGlyph(newGlyph) def layerActions(self): glyph = self.view.glyph() newLayer, action, ok = LayerActionsDialog.getLayerAndAction( self, glyph) if ok and newLayer is not None: # TODO: whole glyph for now, but consider selection too if not glyph.name in newLayer: newLayer.newGlyph(glyph.name) otherGlyph = newLayer[glyph.name] otherGlyph.disableNotifications() if action == "Swap": tempGlyph = TGlyph() otherGlyph.drawPoints(tempGlyph.getPointPen()) tempGlyph.width = otherGlyph.width otherGlyph.clearContours() glyph.drawPoints(otherGlyph.getPointPen()) otherGlyph.width = glyph.width if action != "Copy": glyph.disableNotifications() glyph.clearContours() if action == "Swap": tempGlyph.drawPoints(glyph.getPointPen()) glyph.width = tempGlyph.width glyph.enableNotifications() otherGlyph.enableNotifications() def undo(self): glyph = self.view.glyph() glyph.undo() def redo(self): glyph = self.view.glyph() glyph.redo() def cutOutlines(self): pass def copyOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = QMimeData() copyGlyph = glyph.getRepresentation("defconQt.FilterSelection") mimeData.setData( "application/x-defconQt-glyph-data", pickle.dumps([copyGlyph.serialize(blacklist=("name", "unicode"))])) clipboard.setMimeData(mimeData) def pasteOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-defconQt-glyph-data"): data = pickle.loads( mimeData.data("application/x-defconQt-glyph-data")) if len(data) == 1: pen = glyph.getPointPen() pasteGlyph = TGlyph() pasteGlyph.deserialize(data[0]) # TODO: if we serialize selected state, we don't need to do # this pasteGlyph.selected = True if len(pasteGlyph) or len(pasteGlyph.components) or \ len(pasteGlyph.anchors): glyph.prepareUndo() pasteGlyph.drawPoints(pen) def selectAll(self): glyph = self.view.glyph() glyph.selected = True if not len(glyph): for component in glyph.components: component.selected = True def deselect(self): glyph = self.view.glyph() for anchor in glyph.anchors: anchor.selected = False for component in glyph.components: component.selected = False glyph.selected = False def lockToolBars(self): action = self.sender() movable = not action.isChecked() for toolBar in (self._toolsToolBar, self._buttonsToolBar, self._layersToolBar): toolBar.setMovable(movable) # -------------------------- # Tools & buttons management # -------------------------- def installTool(self, tool): # TODO: add shortcut with number action = self._toolsToolBar.addAction(QIcon(tool.iconPath), tool.name, self._setViewTool) action.setCheckable(True) action.setData(len(self._tools)) self._toolsActionGroup.addAction(action) self._tools.append(tool(parent=self.view)) return action def uninstallTool(self, tool): pass # XXX def _setViewTool(self): action = self.sender() action.setChecked(True) index = action.data() self.view.currentTool = self._tools[index] def installButton(self, button): action = self._buttonsToolBar.addAction(QIcon(button.iconPath), button.name, self._buttonAction) action.setData(len(self._buttons)) self._buttons.append(button(parent=self.view)) return action def uninstallButton(self, button): pass # XXX def _buttonAction(self): action = self.sender() index = action.data() button = self._buttons[index] button.clicked() # -------------------- # Notification support # -------------------- # glyph def _subscribeToGlyph(self, glyph): if glyph is not None: glyph.addObserver(self, "_glyphChanged", "Glyph.Changed") glyph.addObserver(self, "_glyphNameChanged", "Glyph.NameChanged") glyph.addObserver(self, "_glyphSelectionChanged", "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.connect(self._undoAction.setEnabled) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) self._subscribeToFontAndLayerSet(glyph.font) def _unsubscribeFromGlyph(self, glyph): if glyph is not None: glyph.removeObserver(self, "Glyph.Changed") glyph.removeObserver(self, "Glyph.NameChanged") glyph.removeObserver(self, "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.disconnect(self._undoAction.setEnabled) undoManager.canRedoChanged.disconnect(self._redoAction.setEnabled) self._unsubscribeFromFontAndLayerSet(glyph.font) def _glyphChanged(self, notification): self.view.glyphChanged() def _glyphNameChanged(self, notification): glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) def _glyphSelectionChanged(self, notification): self._updateSelection() self.view.glyphChanged() def _fontInfoChanged(self, notification): self.view.fontInfoChanged() glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) # layers & font def _subscribeToFontAndLayerSet(self, font): """Note: called by _subscribeToGlyph.""" if font is None: return font.info.addObserver(self, "_fontInfoChanged", "Info.Changed") layerSet = font.layers if layerSet is None: return layerSet.addObserver(self, '_layerSetLayerDeleted', 'LayerSet.LayerDeleted') for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged'): layerSet.addObserver(self, '_layerSetEvents', event) def _unsubscribeFromFontAndLayerSet(self, font): """Note: called by _unsubscribeFromGlyph.""" if font is None: return font.info.removeObserver(self, "Info.Changed") layerSet = font.layers if layerSet is None: return for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged', 'LayerSet.LayerDeleted'): layerSet.removeObserver(self, event) def _layerSetEvents(self, notification): self._updateLayerControls() def _layerSetLayerDeleted(self, notification): self._layerSetEvents(notification) self._currentLayerBox.setCurrentIndex(0) # other updaters def _updateUndoRedo(self): glyph = self.view.glyph() self._undoAction.setEnabled(glyph.canUndo()) self._redoAction.setEnabled(glyph.canRedo()) def _updateSelection(self): def hasSelection(): glyph = self.view.glyph() for contour in glyph: if len(contour.selection): return True for anchor in glyph.anchors: if anchor.selected: return True for component in glyph.components: if component.selected: return True return False self._copyAction.setEnabled(hasSelection()) # -------------- # Public Methods # -------------- def setGlyph(self, glyph): currentGlyph = self.view.glyph() self._unsubscribeFromGlyph(currentGlyph) self._subscribeToGlyph(glyph) self.view.setGlyph(glyph) self._updateLayerControls() self._updateUndoRedo() self._updateSelection() self.setWindowTitle(glyph.name, glyph.font) def setDrawingAttribute(self, attr, value, layerName=None): self.view.setDrawingAttribute(attr, value, layerName) def drawingAttribute(self, attr, layerName=None): return self.view.drawingAttribute(attr, layerName) # ----------------- # Layers management # ----------------- def _layerChanged(self, newLayerIndex): glyph = self.view.glyph() layer = self._currentLayerBox.itemData(newLayerIndex) if layer is None: layer = self._makeLayer() if layer is None: # restore comboBox to active index layerSet = glyph.layerSet index = layerSet.layerOrder.index(glyph.layer.name) self._setLayerBoxIndex(index) return if glyph.name in layer: newGlyph = layer[glyph.name] else: # TODO: make sure we mimic defcon ufo3 APIs for that newGlyph = self._makeLayerGlyph(layer, glyph) self.setGlyph(newGlyph) # setting the layer-glyph here app = QApplication.instance() app.setCurrentGlyph(newGlyph) def _makeLayer(self): # TODO: what with duplicate names? glyph = self.view.glyph() newLayerName, ok = AddLayerDialog.getNewLayerName(self) if ok: layerSet = glyph.layerSet # TODO: this should return the layer layerSet.newLayer(newLayerName) return layerSet[newLayerName] else: return None def _makeLayerGlyph(self, layer, currentGlyph): glyph = layer.newGlyph(currentGlyph.name) glyph.width = currentGlyph.width glyph.template = True return glyph def _updateLayerControls(self): comboBox = self._currentLayerBox glyph = self.view.glyph() comboBox.blockSignals(True) comboBox.clear() for layer in glyph.layerSet: comboBox.addItem(layer.name, layer) comboBox.setCurrentText(glyph.layer.name) comboBox.addItem("New layer…", None) comboBox.blockSignals(False) self._layerAction.setEnabled(len(glyph.layerSet) > 1) def _setLayerBoxIndex(self, index): comboBox = self._currentLayerBox comboBox.blockSignals(True) comboBox.setCurrentIndex(index) comboBox.blockSignals(False) # --------------------- # QMainWindow functions # --------------------- def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentGlyph(self.view.glyph()) return super().event(event) def closeEvent(self, event): glyph = self.view.glyph() self._unsubscribeFromGlyph(glyph) event.accept() def setWindowTitle(self, title, font=None): if font is not None: title = "%s – %s %s" % (title, font.info.familyName, font.info.styleName) super().setWindowTitle(title)
class MainGlyphWindow(QMainWindow): def __init__(self, glyph, parent=None): super().__init__(parent) menuBar = self.menuBar() fileMenu = QMenu("&File", self) fileMenu.addAction("E&xit", self.close, QKeySequence.Quit) menuBar.addMenu(fileMenu) editMenu = QMenu("&Edit", self) self._undoAction = editMenu.addAction( "&Undo", self.undo, QKeySequence.Undo) self._redoAction = editMenu.addAction( "&Redo", self.redo, QKeySequence.Redo) editMenu.addSeparator() # XXX action = editMenu.addAction("C&ut", self.cutOutlines, QKeySequence.Cut) action.setEnabled(False) self._copyAction = editMenu.addAction( "&Copy", self.copyOutlines, QKeySequence.Copy) editMenu.addAction("&Paste", self.pasteOutlines, QKeySequence.Paste) editMenu.addAction( "Select &All", self.selectAll, QKeySequence.SelectAll) editMenu.addAction("&Deselect", self.deselect, "Ctrl+D") menuBar.addMenu(editMenu) glyphMenu = QMenu("&Glyph", self) glyphMenu.addAction("&Next Glyph", lambda: self.glyphOffset(1), "End") glyphMenu.addAction( "&Previous Glyph", lambda: self.glyphOffset(-1), "Home") glyphMenu.addAction("&Go To…", self.changeGlyph, "G") glyphMenu.addSeparator() self._layerAction = glyphMenu.addAction( "&Layer Actions…", self.layerActions, "L") menuBar.addMenu(glyphMenu) # create tools and buttons toolBars self._tools = [] self._toolsToolBar = QToolBar("Tools", self) self._toolsToolBar.setMovable(False) self._buttons = [] self._buttonsToolBar = QToolBar("Buttons", self) self._buttonsToolBar.setMovable(False) self.addToolBar(self._toolsToolBar) self.addToolBar(self._buttonsToolBar) # http://www.setnode.com/blog/right-aligning-a-button-in-a-qtoolbar/ self._layersToolBar = QToolBar("Layers", self) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._currentLayerBox = QComboBox(self) self._currentLayerBox.currentIndexChanged.connect( self._layerChanged) self._layersToolBar.addWidget(spacer) self._layersToolBar.addWidget(self._currentLayerBox) self._layersToolBar.setContentsMargins(0, 0, 2, 0) self._layersToolBar.setMovable(False) self.addToolBar(self._layersToolBar) viewMenu = self.createPopupMenu() viewMenu.setTitle("View") viewMenu.addSeparator() action = viewMenu.addAction("Lock Toolbars", self.lockToolBars) action.setCheckable(True) action.setChecked(True) menuBar.addMenu(viewMenu) self.view = GlyphView(self) self.setGlyph(glyph) selectionTool = self.installTool(SelectionTool) selectionTool.trigger() self.installTool(PenTool) self.installTool(RulerTool) self.installTool(KnifeTool) self.installButton(RemoveOverlapButton) self.setCentralWidget(self.view.scrollArea()) self.resize(900, 700) self.view.setFocus(True) # ---------- # Menu items # ---------- def glyphOffset(self, offset): currentGlyph = self.view.glyph() font = currentGlyph.font glyphOrder = font.glyphOrder # should be enforced in fontView already if not (glyphOrder and len(glyphOrder)): return index = glyphOrder.index(currentGlyph.name) newIndex = (index + offset) % len(glyphOrder) glyph = font[glyphOrder[newIndex]] self.setGlyph(glyph) def changeGlyph(self): glyph = self.view.glyph() newGlyph, ok = GotoDialog.getNewGlyph(self, glyph) if ok and newGlyph is not None: self.setGlyph(newGlyph) def layerActions(self): glyph = self.view.glyph() newLayer, action, ok = LayerActionsDialog.getLayerAndAction( self, glyph) if ok and newLayer is not None: # TODO: whole glyph for now, but consider selection too if not glyph.name in newLayer: newLayer.newGlyph(glyph.name) otherGlyph = newLayer[glyph.name] otherGlyph.disableNotifications() if action == "Swap": tempGlyph = TGlyph() otherGlyph.drawPoints(tempGlyph.getPointPen()) tempGlyph.width = otherGlyph.width otherGlyph.clearContours() glyph.drawPoints(otherGlyph.getPointPen()) otherGlyph.width = glyph.width if action != "Copy": glyph.disableNotifications() glyph.clearContours() if action == "Swap": tempGlyph.drawPoints(glyph.getPointPen()) glyph.width = tempGlyph.width glyph.enableNotifications() otherGlyph.enableNotifications() def undo(self): glyph = self.view.glyph() glyph.undo() def redo(self): glyph = self.view.glyph() glyph.redo() def cutOutlines(self): pass def copyOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = QMimeData() copyGlyph = glyph.getRepresentation("defconQt.FilterSelection") mimeData.setData("application/x-defconQt-glyph-data", pickle.dumps([copyGlyph.serialize( blacklist=("name", "unicode") )])) clipboard.setMimeData(mimeData) def pasteOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-defconQt-glyph-data"): data = pickle.loads(mimeData.data( "application/x-defconQt-glyph-data")) if len(data) == 1: pen = glyph.getPointPen() pasteGlyph = TGlyph() pasteGlyph.deserialize(data[0]) # TODO: if we serialize selected state, we don't need to do # this pasteGlyph.selected = True if len(pasteGlyph) or len(pasteGlyph.components) or \ len(pasteGlyph.anchors): glyph.prepareUndo() pasteGlyph.drawPoints(pen) def selectAll(self): glyph = self.view.glyph() glyph.selected = True if not len(glyph): for component in glyph.components: component.selected = True def deselect(self): glyph = self.view.glyph() for anchor in glyph.anchors: anchor.selected = False for component in glyph.components: component.selected = False glyph.selected = False def lockToolBars(self): action = self.sender() movable = not action.isChecked() for toolBar in ( self._toolsToolBar, self._buttonsToolBar, self._layersToolBar): toolBar.setMovable(movable) # -------------------------- # Tools & buttons management # -------------------------- def installTool(self, tool): action = self._toolsToolBar.addAction( QIcon(tool.iconPath), tool.name, self._setViewTool) action.setCheckable(True) num = len(self._tools) action.setData(num) action.setShortcut(QKeySequence(str(num + 1))) self._tools.append(tool(parent=self.view)) return action def uninstallTool(self, tool): pass # XXX def _setViewTool(self): action = self.sender() index = action.data() newTool = self._tools[index] if newTool == self.view.currentTool(): action.setChecked(True) return ok = self.view.setCurrentTool(newTool) # if view did change tool, disable them all and enable the one we want # otherwise, just disable the tool that was clicked. # previously we used QActionGroup to have exclusive buttons, but doing # it manually allows us to NAK a button change. if ok: for act in self._toolsToolBar.actions(): act.setChecked(False) action.setChecked(ok) def installButton(self, button): action = self._buttonsToolBar.addAction( QIcon(button.iconPath), button.name, self._buttonAction) action.setData(len(self._buttons)) self._buttons.append(button(parent=self.view)) return action def uninstallButton(self, button): pass # XXX def _buttonAction(self): action = self.sender() index = action.data() button = self._buttons[index] button.clicked() # -------------------- # Notification support # -------------------- # glyph def _subscribeToGlyph(self, glyph): if glyph is not None: glyph.addObserver(self, "_glyphChanged", "Glyph.Changed") glyph.addObserver(self, "_glyphNameChanged", "Glyph.NameChanged") glyph.addObserver( self, "_glyphSelectionChanged", "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.connect(self._undoAction.setEnabled) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) self._subscribeToFontAndLayerSet(glyph.font) def _unsubscribeFromGlyph(self, glyph): if glyph is not None: glyph.removeObserver(self, "Glyph.Changed") glyph.removeObserver(self, "Glyph.NameChanged") glyph.removeObserver(self, "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.disconnect(self._undoAction.setEnabled) undoManager.canRedoChanged.disconnect(self._redoAction.setEnabled) self._unsubscribeFromFontAndLayerSet(glyph.font) def _glyphChanged(self, notification): self.view.glyphChanged() def _glyphNameChanged(self, notification): glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) def _glyphSelectionChanged(self, notification): self._updateSelection() self.view.glyphChanged() def _fontInfoChanged(self, notification): self.view.fontInfoChanged() glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) # layers & font def _subscribeToFontAndLayerSet(self, font): """Note: called by _subscribeToGlyph.""" if font is None: return font.info.addObserver(self, "_fontInfoChanged", "Info.Changed") layerSet = font.layers if layerSet is None: return layerSet.addObserver(self, '_layerSetLayerDeleted', 'LayerSet.LayerDeleted') for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged'): layerSet.addObserver(self, '_layerSetEvents', event) def _unsubscribeFromFontAndLayerSet(self, font): """Note: called by _unsubscribeFromGlyph.""" if font is None: return font.info.removeObserver(self, "Info.Changed") layerSet = font.layers if layerSet is None: return for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged', 'LayerSet.LayerDeleted'): layerSet.removeObserver(self, event) def _layerSetEvents(self, notification): self._updateLayerControls() def _layerSetLayerDeleted(self, notification): self._layerSetEvents(notification) self._currentLayerBox.setCurrentIndex(0) # other updaters def _updateUndoRedo(self): glyph = self.view.glyph() self._undoAction.setEnabled(glyph.canUndo()) self._redoAction.setEnabled(glyph.canRedo()) def _updateSelection(self): def hasSelection(): glyph = self.view.glyph() for contour in glyph: if len(contour.selection): return True for anchor in glyph.anchors: if anchor.selected: return True for component in glyph.components: if component.selected: return True return False self._copyAction.setEnabled(hasSelection()) # -------------- # Public Methods # -------------- def setGlyph(self, glyph): currentGlyph = self.view.glyph() self._unsubscribeFromGlyph(currentGlyph) self._subscribeToGlyph(glyph) self.view.setGlyph(glyph) self._updateLayerControls() self._updateUndoRedo() self._updateSelection() self.setWindowTitle(glyph.name, glyph.font) def setDrawingAttribute(self, attr, value, layerName=None): self.view.setDrawingAttribute(attr, value, layerName) def drawingAttribute(self, attr, layerName=None): return self.view.drawingAttribute(attr, layerName) # ----------------- # Layers management # ----------------- def _layerChanged(self, newLayerIndex): glyph = self.view.glyph() layer = self._currentLayerBox.itemData(newLayerIndex) if layer is None: layer = self._makeLayer() if layer is None: # restore comboBox to active index layerSet = glyph.layerSet index = layerSet.layerOrder.index(glyph.layer.name) self._setLayerBoxIndex(index) return if glyph.name in layer: newGlyph = layer[glyph.name] else: # TODO: make sure we mimic defcon ufo3 APIs for that newGlyph = self._makeLayerGlyph(layer, glyph) self.setGlyph(newGlyph) # setting the layer-glyph here app = QApplication.instance() app.setCurrentGlyph(newGlyph) def _makeLayer(self): # TODO: what with duplicate names? glyph = self.view.glyph() newLayerName, ok = AddLayerDialog.getNewLayerName(self) if ok: layerSet = glyph.layerSet # TODO: this should return the layer layerSet.newLayer(newLayerName) return layerSet[newLayerName] else: return None def _makeLayerGlyph(self, layer, currentGlyph): glyph = layer.newGlyph(currentGlyph.name) glyph.width = currentGlyph.width glyph.template = True return glyph def _updateLayerControls(self): comboBox = self._currentLayerBox glyph = self.view.glyph() comboBox.blockSignals(True) comboBox.clear() for layer in glyph.layerSet: comboBox.addItem(layer.name, layer) comboBox.setCurrentText(glyph.layer.name) comboBox.addItem("New layer…", None) comboBox.blockSignals(False) self._layerAction.setEnabled(len(glyph.layerSet) > 1) def _setLayerBoxIndex(self, index): comboBox = self._currentLayerBox comboBox.blockSignals(True) comboBox.setCurrentIndex(index) comboBox.blockSignals(False) # --------------------- # QMainWindow functions # --------------------- def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentGlyph(self.view.glyph()) return super().event(event) def closeEvent(self, event): glyph = self.view.glyph() self._unsubscribeFromGlyph(glyph) event.accept() def setWindowTitle(self, title, font=None): if font is not None: title = "%s – %s %s" % ( title, font.info.familyName, font.info.styleName) super().setWindowTitle(title)
class TransCodaEditor(QWidget): encoder_changed = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') def __init__(self, caption="Trans:Coda Editor"): super().__init__() self.encoder = EncoderSelector() self.encoder_view = EncoderView() self.actions = QToolBar() self.caption = caption self.add_action = CommonUtils.create_action( tooltip="Add Encoder", icon=TransCoda.theme.ico_add_item, func=self.action_add, name="Add", parent=self) self.del_action = CommonUtils.create_action( tooltip="Delete Encoder", icon=TransCoda.theme.ico_clear, func=self.action_del, name="Delete", parent=self) self.mod_action = CommonUtils.create_action( tooltip="Edit Encoder", icon=TransCoda.theme.ico_edit, func=self.action_mod, name="Edit", parent=self) self.ref_action = CommonUtils.create_action( tooltip="Refresh Encoder", icon=TransCoda.theme.ico_refresh, func=self.action_ref, name="Refresh", parent=self) self._init_ui() def _init_ui(self): self.encoder.encoder_changed.connect(self._configure_toolbar) self.actions.setOrientation(Qt.Horizontal) self.actions.addActions([ self.add_action, self.del_action, self.mod_action, self.ref_action ]) self.actions.setContentsMargins(0, 0, 0, 0) q_size = QSize(16, 16) self.actions.setIconSize(q_size) self._configure_toolbar(None, None) h_layout = QHBoxLayout() h_layout.setContentsMargins(0, 0, 0, 0) h_layout.addWidget(QLabel(f"<u>{self.caption}</u>"), 1) h_layout.addWidget(self.actions) v_layout = QVBoxLayout() v_layout.setContentsMargins(0, 0, 0, 0) v_layout.addLayout(h_layout) v_layout.addWidget(self.encoder) self.setMinimumWidth(400) self.setLayout(v_layout) def action_add(self, _): self.encoder_view.update_view("Add Encoder") if self.encoder_view.exec() == QDialog.Accepted: media_type, encoder_group, name, command, executable, extension = self.encoder_view.get_details( ) self.encoder.add_encoder(media_type, encoder_group, name, command, executable, extension) def action_del(self, _): path, _ = self.encoder.get_encoder() tokens = path.split(EncoderSelector.__PATH_SEPARATOR__) self.encoder.del_encoder(tokens[0], tokens[1], tokens[2]) def action_mod(self, _): path, encoder = self.encoder.get_encoder() self.encoder_view.update_view("Modify Encoder", encoder_path=path, encoder=encoder) if self.encoder_view.exec() == QDialog.Accepted: media_type, encoder_group, name, command, executable, extension = self.encoder_view.get_details( ) self.encoder.update_encoder(media_type, encoder_group, name, command, executable, extension) def action_ref(self, _): self.encoder.refresh_encoders() def select_encoder(self, encoder_name): return self.encoder.select_encoder(encoder_name) def _configure_toolbar(self, path, encoder): self.mod_action.setEnabled(True if encoder else False) self.del_action.setEnabled(True if encoder else False) if encoder: self.encoder_changed.emit(path, encoder)
class JavaTracePanel(QWidget): def __init__(self, app, *__args): super().__init__(app) self.app = app self.app.dwarf.onJavaTraceEvent.connect(self.on_event) self.app.dwarf.onEnumerateJavaClassesStart.connect( self.on_enumeration_start) self.app.dwarf.onEnumerateJavaClassesMatch.connect( self.on_enumeration_match) self.app.dwarf.onEnumerateJavaClassesComplete.connect( self.on_enumeration_complete) self.tracing = False self.trace_classes = [] self.trace_depth = 0 layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self._record_icon = QIcon( utils.resource_path('assets/icons/record.png')) self._pause_icon = QIcon(utils.resource_path('assets/icons/pause.png')) self._stop_icon = QIcon(utils.resource_path('assets/icons/stop.png')) self._tool_bar = QToolBar() self._tool_bar.setFixedHeight(16) self._tool_bar.setContentsMargins(1, 1, 1, 1) self._tool_bar.addAction(self._record_icon, 'Record', self.start_trace) self._tool_bar.addAction(self._pause_icon, 'Pause', self.pause_trace) self._tool_bar.addAction(self._stop_icon, 'Stop', self.stop_trace) self._tool_bar.addSeparator() self._entries_lbl = QLabel('Entries: 0') self._entries_lbl.setAlignment(Qt.AlignRight) self._entries_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self._tool_bar.addWidget(self._entries_lbl) layout.addWidget(self._tool_bar) self.setup_splitter = QSplitter() self.events_list = JavaTraceView(self) self.events_list.setVisible(False) self.trace_list = QListWidget() self.class_list = QListWidget() self.trace_list.itemDoubleClicked.connect(self.trace_list_double_click) self.class_list.setContextMenuPolicy(Qt.CustomContextMenu) self.class_list.customContextMenuRequested.connect( self.show_class_list_menu) self.class_list.itemDoubleClicked.connect(self.class_list_double_click) self.current_class_search = '' bar = QScrollBar() bar.setFixedWidth(0) bar.setFixedHeight(0) self.trace_list.setHorizontalScrollBar(bar) bar = QScrollBar() bar.setFixedWidth(0) bar.setFixedHeight(0) self.class_list.setHorizontalScrollBar(bar) self.setup_splitter.addWidget(self.trace_list) self.setup_splitter.addWidget(self.class_list) self.setup_splitter.setHandleWidth(1) layout.addWidget(self.setup_splitter) layout.addWidget(self.events_list) self.setLayout(layout) def class_list_double_click(self, item): try: if self.trace_classes.index(item.text()) >= 0: return except: pass self.trace_classes.append(item.text()) q = NotEditableListWidgetItem(item.text()) self.trace_list.addItem(q) self.trace_list.sortItems() def on_enumeration_start(self): self.class_list.clear() def on_enumeration_match(self, java_class): try: if PREFIXED_CLASS.index(java_class) >= 0: try: if self.trace_classes.index(java_class) >= 0: return except: pass q = NotEditableListWidgetItem(java_class) self.trace_list.addItem(q) self.trace_classes.append(java_class) except: pass q = NotEditableListWidgetItem(java_class) self.class_list.addItem(q) def on_enumeration_complete(self): self.class_list.sortItems() self.trace_list.sortItems() def on_event(self, data): trace, event, clazz, data = data if trace == 'java_trace': self.events_list.add_event({ 'event': event, 'class': clazz, 'data': data.replace(',', ', ') }) self._entries_lbl.setText('Events: %d' % len(self.events_list.data)) def pause_trace(self): self.app.dwarf.dwarf_api('stopJavaTracer') self.tracing = False def search(self): accept, input = InputDialog.input( self.app, hint='Search', input_content=self.current_class_search, placeholder='Search something...') if accept: self.current_class_search = input.lower() for i in range(0, self.class_list.count()): try: if self.class_list.item(i).text().lower().index( self.current_class_search.lower()) >= 0: self.class_list.setRowHidden(i, False) except: self.class_list.setRowHidden(i, True) def show_class_list_menu(self, pos): menu = QMenu() search = menu.addAction('Search') action = menu.exec_(self.class_list.mapToGlobal(pos)) if action: if action == search: self.search() def start_trace(self): self.app.dwarf.dwarf_api('startJavaTracer', [self.trace_classes]) self.trace_depth = 0 self.tracing = True self.setup_splitter.setVisible(False) self.events_list.setVisible(True) def stop_trace(self): self.app.dwarf.dwarf_api('stopJavaTracer') self.tracing = False self.setup_splitter.setVisible(True) self.events_list.setVisible(False) self.events_list.clear() def trace_list_double_click(self, item): try: index = self.trace_classes.index(item.text()) except: return if index < 0: return self.trace_classes.pop(index) self.trace_list.takeItem(self.trace_list.row(item)) def keyPressEvent(self, event): if event.modifiers() & Qt.ControlModifier: if event.key() == Qt.Key_F: self.search() super(JavaTracePanel, self).keyPressEvent(event)
class GlyphWindow(BaseMainWindow): def __init__(self, glyph, parent=None): super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose, False) self.setUnifiedTitleAndToolBarOnMac(True) self.view = GlyphCanvasView(self) # create tools and buttons toolBars self._tools = [] self._toolsToolBar = QToolBar(self.tr("Tools"), self) self._toolsToolBar.setMovable(False) self._buttons = [] self._buttonsToolBar = QToolBar(self.tr("Buttons"), self) self._buttonsToolBar.setMovable(False) self.addToolBar(self._toolsToolBar) self.addToolBar(self._buttonsToolBar) # http://www.setnode.com/blog/right-aligning-a-button-in-a-qtoolbar/ self._layersToolBar = QToolBar(self.tr("Layers"), self) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._currentLayerBox = QComboBox(self) self._currentLayerBox.currentIndexChanged.connect( self._layerChanged) self._layersToolBar.addWidget(spacer) self._layersToolBar.addWidget(self._currentLayerBox) self._layersToolBar.setContentsMargins(0, 0, 2, 0) self._layersToolBar.setMovable(False) self.addToolBar(self._layersToolBar) self.setGlyph(glyph) app = QApplication.instance() tools = app.drawingTools() for index, tool in enumerate(tools): action = self.installTool(tool) if not index: action.trigger() app.dispatcher.addObserver( self, "_drawingToolRegistered", "drawingToolRegistered") # TODO: drawingToolUnregistered self.installButton(RemoveOverlapButton) self.setCentralWidget(self.view) self.view.setFocus(True) self.readSettings() def readSettings(self): geometry = settings.glyphWindowGeometry() if geometry: self.restoreGeometry(geometry) def writeSettings(self): # TODO: save current tool? settings.setGlyphWindowGeometry(self.saveGeometry()) def setupMenu(self, menuBar): fileMenu = menuBar.fetchMenu(Entries.File) fontWindow = self.parent() if fontWindow is not None: fileMenu.fetchAction(Entries.File_Save, fontWindow.saveFile) fileMenu.fetchAction(Entries.File_Save_As, fontWindow.saveFileAs) fileMenu.fetchAction(Entries.File_Close, self.close) if fontWindow is not None: fileMenu.fetchAction(Entries.File_Reload, fontWindow.reloadFile) editMenu = menuBar.fetchMenu(Entries.Edit) self._undoAction = editMenu.fetchAction(Entries.Edit_Undo, self.undo) self._redoAction = editMenu.fetchAction(Entries.Edit_Redo, self.redo) editMenu.addSeparator() cutAction = editMenu.fetchAction(Entries.Edit_Cut, self.cutOutlines) copyAction = editMenu.fetchAction(Entries.Edit_Copy, self.copyOutlines) self._selectActions = (cutAction, copyAction) editMenu.fetchAction(Entries.Edit_Paste, self.pasteOutlines) editMenu.fetchAction(Entries.Edit_Select_All, self.selectAll) editMenu.fetchAction(Entries.Edit_Find, self.changeGlyph) # TODO: sort this out # editMenu.fetchAction(self.tr("&Deselect"), self.deselect, "Ctrl+D") # glyphMenu = menuBar.fetchMenu(self.tr("&Glyph")) # self._layerAction = glyphMenu.fetchAction( # self.tr("&Layer Actions…"), self.layerActions, "L") viewMenu = menuBar.fetchMenu(Entries.View) viewMenu.fetchAction(Entries.View_Zoom_In, lambda: self.view.zoom(1)) viewMenu.fetchAction(Entries.View_Zoom_Out, lambda: self.view.zoom(-1)) viewMenu.fetchAction(Entries.View_Reset_Zoom, self.view.fitScaleBBox) viewMenu.addSeparator() viewMenu.fetchAction( Entries.View_Next_Glyph, lambda: self.glyphOffset(1)) viewMenu.fetchAction( Entries.View_Previous_Glyph, lambda: self.glyphOffset(-1)) self._updateUndoRedo() # ---------- # Menu items # ---------- def saveFile(self): glyph = self.view.glyph() font = glyph.font if None not in (font, font.path): font.save() def glyphOffset(self, offset): currentGlyph = self.view.glyph() font = currentGlyph.font glyphOrder = font.glyphOrder # should be enforced in fontView already if not (glyphOrder and len(glyphOrder)): return index = glyphOrder.index(currentGlyph.name) newIndex = (index + offset) % len(glyphOrder) glyph = font[glyphOrder[newIndex]] self.setGlyph(glyph) def changeGlyph(self): glyph = self.view.glyph() newGlyph, ok = FindDialog.getNewGlyph(self, glyph) if ok and newGlyph is not None: self.setGlyph(newGlyph) def layerActions(self): glyph = self.view.glyph() newLayer, action, ok = LayerActionsDialog.getLayerAndAction( self, glyph) if ok and newLayer is not None: # TODO: whole glyph for now, but consider selection too if glyph.name not in newLayer: newLayer.newGlyph(glyph.name) otherGlyph = newLayer[glyph.name] otherGlyph.holdNotifications() if action == "Swap": tempGlyph = glyph.__class__() otherGlyph.drawPoints(tempGlyph.getPointPen()) tempGlyph.width = otherGlyph.width otherGlyph.clearContours() glyph.drawPoints(otherGlyph.getPointPen()) otherGlyph.width = glyph.width if action != "Copy": glyph.holdNotifications() glyph.clearContours() if action == "Swap": tempGlyph.drawPoints(glyph.getPointPen()) glyph.width = tempGlyph.width glyph.releaseHeldNotifications() otherGlyph.releaseHeldNotifications() def undo(self): glyph = self.view.glyph() glyph.undo() def redo(self): glyph = self.view.glyph() glyph.redo() def cutOutlines(self): glyph = self.view.glyph() self.copyOutlines() deleteUISelection(glyph) def copyOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = QMimeData() copyGlyph = glyph.getRepresentation("TruFont.FilterSelection") mimeData.setData("application/x-trufont-glyph-data", pickle.dumps([copyGlyph.serialize( blacklist=("name", "unicode") )])) clipboard.setMimeData(mimeData) def pasteOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-trufont-glyph-data"): data = pickle.loads(mimeData.data( "application/x-trufont-glyph-data")) if len(data) == 1: pen = glyph.getPointPen() pasteGlyph = glyph.__class__() pasteGlyph.deserialize(data[0]) # TODO: if we serialize selected state, we don't need to do # this pasteGlyph.selected = True if len(pasteGlyph) or len(pasteGlyph.components) or \ len(pasteGlyph.anchors): glyph.prepareUndo() pasteGlyph.drawPoints(pen) def selectAll(self): glyph = self.view.glyph() if glyph.selected: for anchor in glyph.anchors: anchor.selected = True for component in glyph.components: component.selected = True else: glyph.selected = True def deselect(self): glyph = self.view.glyph() for anchor in glyph.anchors: anchor.selected = False for component in glyph.components: component.selected = False glyph.selected = False def lockToolBars(self): action = self.sender() movable = not action.isChecked() for toolBar in ( self._toolsToolBar, self._buttonsToolBar, self._layersToolBar): toolBar.setMovable(movable) # -------------------------- # Tools & buttons management # -------------------------- def installTool(self, tool): action = self._toolsToolBar.addAction( QIcon(tool.iconPath), tool.name, self._setViewTool) action.setCheckable(True) num = len(self._tools) action.setData(num) action.setShortcut(QKeySequence(str(num + 1))) self._tools.append(tool(parent=self.view.widget())) return action def uninstallTool(self, tool): pass # XXX def _setViewTool(self): action = self.sender() index = action.data() newTool = self._tools[index] if newTool == self.view.currentTool(): action.setChecked(True) return ok = self.view.setCurrentTool(newTool) # if view did change tool, disable them all and enable the one we want # otherwise, just disable the tool that was clicked. # previously we used QActionGroup to have exclusive buttons, but doing # it manually allows us to NAK a button change. if ok: for act in self._toolsToolBar.actions(): act.setChecked(False) action.setChecked(ok) def installButton(self, button): action = self._buttonsToolBar.addAction( QIcon(button.iconPath), button.name, self._buttonAction) action.setData(len(self._buttons)) self._buttons.append(button(parent=self.view)) return action def uninstallButton(self, button): pass # XXX def _buttonAction(self): action = self.sender() index = action.data() button = self._buttons[index] button.clicked() # ------------- # Notifications # ------------- # app def _drawingToolRegistered(self, notification): tool = notification.data["tool"] self.installTool(tool) # glyph def _subscribeToGlyph(self, glyph): if glyph is not None: glyph.addObserver(self, "_glyphNameChanged", "Glyph.NameChanged") glyph.addObserver( self, "_glyphSelectionChanged", "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.connect(self._setUndoEnabled) undoManager.canRedoChanged.connect(self._setRedoEnabled) self._subscribeToFontAndLayerSet(glyph.font) def _unsubscribeFromGlyph(self, glyph): if glyph is not None: glyph.removeObserver(self, "Glyph.NameChanged") glyph.removeObserver(self, "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.disconnect(self._setUndoEnabled) undoManager.canRedoChanged.disconnect(self._setRedoEnabled) self._unsubscribeFromFontAndLayerSet(glyph.font) def _glyphNameChanged(self, notification): glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) def _glyphSelectionChanged(self, notification): self._updateSelection() # layers & font def _subscribeToFontAndLayerSet(self, font): """Note: called by _subscribeToGlyph.""" if font is None: return font.info.addObserver(self, "_fontInfoChanged", "Info.Changed") layerSet = font.layers if layerSet is None: return layerSet.addObserver( self, '_layerSetLayerDeleted', 'LayerSet.LayerDeleted') for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged'): layerSet.addObserver(self, '_layerSetEvents', event) def _unsubscribeFromFontAndLayerSet(self, font): """Note: called by _unsubscribeFromGlyph.""" if font is None: return font.info.removeObserver(self, "Info.Changed") layerSet = font.layers if layerSet is None: return for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged', 'LayerSet.LayerDeleted'): layerSet.removeObserver(self, event) def _fontInfoChanged(self, notification): glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) def _layerSetEvents(self, notification): self._updateLayerControls() def _layerSetLayerDeleted(self, notification): self._layerSetEvents(notification) self._currentLayerBox.setCurrentIndex(0) # other updaters def _updateSelection(self): def hasSelection(): glyph = self.view.glyph() for contour in glyph: if len(contour.selection): return True for anchor in glyph.anchors: if anchor.selected: return True for component in glyph.components: if component.selected: return True return False if not hasattr(self, "_selectActions"): return hasSelection = hasSelection() for action in self._selectActions: action.setEnabled(hasSelection) def _updateUndoRedo(self): glyph = self.view.glyph() self._setUndoEnabled(glyph.canUndo()) self._setRedoEnabled(glyph.canRedo()) def _setUndoEnabled(self, value): if not hasattr(self, "_undoAction"): return self._undoAction.setEnabled(value) def _setRedoEnabled(self, value): if not hasattr(self, "_redoAction"): return self._redoAction.setEnabled(value) # -------------- # Public Methods # -------------- def setGlyph(self, glyph): currentGlyph = self.view.glyph() self._unsubscribeFromGlyph(currentGlyph) self.view.setGlyph(glyph) self._subscribeToGlyph(glyph) self._updateLayerControls() self._updateUndoRedo() self._updateSelection() self.setWindowTitle(glyph.name, glyph.font) # setting the layer-glyph here app = QApplication.instance() app.setCurrentGlyph(glyph) # ----------------- # Layers management # ----------------- def _layerChanged(self, newLayerIndex): glyph = self.view.glyph() layer = self._currentLayerBox.itemData(newLayerIndex) if layer is None: layer = self._makeLayer() if layer is None: # restore comboBox to active index layerSet = glyph.layerSet index = layerSet.layerOrder.index(glyph.layer.name) self._setLayerBoxIndex(index) return if glyph.name in layer: newGlyph = layer[glyph.name] else: # TODO: make sure we mimic defcon ufo3 APIs for that newGlyph = self._makeLayerGlyph(layer, glyph) self.setGlyph(newGlyph) def _makeLayer(self): # TODO: what with duplicate names? glyph = self.view.glyph() newLayerName, color, ok = AddLayerDialog.getNewLayerNameAndColor(self) if ok: layerSet = glyph.layerSet layer = layerSet.newLayer(newLayerName) layer.color = color return layer return None def _makeLayerGlyph(self, layer, currentGlyph): glyph = layer.newGlyph(currentGlyph.name) glyph.width = currentGlyph.width glyph.template = True return glyph def _updateLayerControls(self): comboBox = self._currentLayerBox glyph = self.view.glyph() comboBox.blockSignals(True) comboBox.clear() for layer in glyph.layerSet: comboBox.addItem(layer.name, layer) comboBox.setCurrentText(glyph.layer.name) comboBox.addItem(self.tr("New layer…"), None) comboBox.blockSignals(False) if not hasattr(self, "_layerAction"): return self._layerAction.setEnabled(len(glyph.layerSet) > 1) def _setLayerBoxIndex(self, index): comboBox = self._currentLayerBox comboBox.blockSignals(True) comboBox.setCurrentIndex(index) comboBox.blockSignals(False) # --------------------- # QMainWindow functions # --------------------- def sizeHint(self): return QSize(1100, 750) def moveEvent(self, event): self.writeSettings() resizeEvent = moveEvent def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentGlyph(self.view.glyph()) return super().event(event) def showEvent(self, event): app = QApplication.instance() data = dict(window=self) app.postNotification("glyphWindowWillOpen", data) super().showEvent(event) app.postNotification("glyphWindowOpened", data) def closeEvent(self, event): super().closeEvent(event) if event.isAccepted(): app = QApplication.instance() app.dispatcher.removeObserver(self, "drawingToolRegistered") data = dict(window=self) app.postNotification("glyphWindowWillClose", data) glyph = self.view.glyph() self._unsubscribeFromGlyph(glyph) self.view.closeEvent(event) def setWindowTitle(self, title, font=None): if font is not None: title = "%s – %s %s" % ( title, font.info.familyName, font.info.styleName) super().setWindowTitle(title)
class ManageWindow(QWidget): __BASE_HEIGHT__ = 400 signal_user_res = pyqtSignal(bool) signal_table_update = pyqtSignal() def __init__(self, i18n: dict, icon_cache: MemoryCache, manager: SoftwareManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, display_limit: int, config: Configuration, context: ApplicationContext, notifications: bool, tray_icon=None): super(ManageWindow, self).__init__() self.i18n = i18n self.manager = manager self.tray_icon = tray_icon self.working = False # restrict the number of threaded actions self.pkgs = [] # packages current loaded in the table self.pkgs_available = [] # all packages loaded in memory self.pkgs_installed = [] # cached installed packages self.display_limit = display_limit self.icon_cache = icon_cache self.disk_cache = disk_cache self.download_icons = download_icons self.screen_size = screen_size self.config = config self.context = context self.notifications = notifications self.icon_app = QIcon(resource.get_path('img/logo.svg')) self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) self.setWindowIcon(self.icon_app) self.layout = QVBoxLayout() self.setLayout(self.layout) self.toolbar_top = QToolBar() self.toolbar_top.addWidget(new_spacer()) self.label_status = QLabel() self.label_status.setText('') self.label_status.setStyleSheet("font-weight: bold") self.toolbar_top.addWidget(self.label_status) self.toolbar_search = QToolBar() self.toolbar_search.setStyleSheet("spacing: 0px;") self.toolbar_search.setContentsMargins(0, 0, 0, 0) label_pre_search = QLabel() label_pre_search.setStyleSheet( "background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;" ) self.toolbar_search.addWidget(label_pre_search) self.input_search = QLineEdit() self.input_search.setMaxLength(20) self.input_search.setFrame(False) self.input_search.setPlaceholderText( self.i18n['window_manage.input_search.placeholder'] + "...") self.input_search.setToolTip( self.i18n['window_manage.input_search.tooltip']) self.input_search.setStyleSheet( "QLineEdit { background-color: white; color: gray; spacing: 0; height: 30px; font-size: 12px; width: 300px}" ) self.input_search.returnPressed.connect(self.search) self.toolbar_search.addWidget(self.input_search) label_pos_search = QLabel() label_pos_search.setPixmap(QPixmap( resource.get_path('img/search.svg'))) label_pos_search.setStyleSheet( "background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;" ) self.toolbar_search.addWidget(label_pos_search) self.ref_toolbar_search = self.toolbar_top.addWidget( self.toolbar_search) self.toolbar_top.addWidget(new_spacer()) self.layout.addWidget(self.toolbar_top) self.toolbar = QToolBar() self.toolbar.setStyleSheet( 'QToolBar {spacing: 4px; margin-top: 15px; margin-bottom: 5px}') self.checkbox_updates = QCheckBox() self.checkbox_updates.setText(self.i18n['updates'].capitalize()) self.checkbox_updates.stateChanged.connect(self._handle_updates_filter) self.ref_checkbox_updates = self.toolbar.addWidget( self.checkbox_updates) self.checkbox_only_apps = QCheckBox() self.checkbox_only_apps.setText( self.i18n['manage_window.checkbox.only_apps']) self.checkbox_only_apps.setChecked(True) self.checkbox_only_apps.stateChanged.connect( self._handle_filter_only_apps) self.ref_checkbox_only_apps = self.toolbar.addWidget( self.checkbox_only_apps) self.any_type_filter = 'any' self.cache_type_filter_icons = {} self.combo_filter_type = QComboBox() self.combo_filter_type.setStyleSheet('QLineEdit { height: 2px}') self.combo_filter_type.setEditable(True) self.combo_filter_type.lineEdit().setReadOnly(True) self.combo_filter_type.lineEdit().setAlignment(Qt.AlignCenter) self.combo_filter_type.activated.connect(self._handle_type_filter) self.combo_filter_type.addItem( load_icon(resource.get_path('img/logo.svg'), 14), self.i18n[self.any_type_filter].capitalize(), self.any_type_filter) self.ref_combo_filter_type = self.toolbar.addWidget( self.combo_filter_type) self.input_name_filter = InputFilter(self.apply_filters_async) self.input_name_filter.setMaxLength(10) self.input_name_filter.setPlaceholderText( self.i18n['manage_window.name_filter.placeholder'] + '...') self.input_name_filter.setToolTip( self.i18n['manage_window.name_filter.tooltip']) self.input_name_filter.setStyleSheet( "QLineEdit { background-color: white; color: gray;}") self.input_name_filter.setFixedWidth(130) self.ref_input_name_filter = self.toolbar.addWidget( self.input_name_filter) self.toolbar.addWidget(new_spacer()) self.bt_installed = QPushButton() self.bt_installed.setToolTip( self.i18n['manage_window.bt.installed.tooltip']) self.bt_installed.setIcon(QIcon(resource.get_path('img/disk.png'))) self.bt_installed.setText( self.i18n['manage_window.bt.installed.text'].capitalize()) self.bt_installed.clicked.connect(self._show_installed) self.bt_installed.setStyleSheet(toolbar_button_style('#A94E0A')) self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed) self.bt_refresh = QPushButton() self.bt_refresh.setToolTip(i18n['manage_window.bt.refresh.tooltip']) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.setText(self.i18n['manage_window.bt.refresh.text']) self.bt_refresh.setStyleSheet(toolbar_button_style('#2368AD')) self.bt_refresh.clicked.connect( lambda: self.refresh_apps(keep_console=False)) self.ref_bt_refresh = self.toolbar.addWidget(self.bt_refresh) self.bt_upgrade = QPushButton() self.bt_upgrade.setToolTip(i18n['manage_window.bt.upgrade.tooltip']) self.bt_upgrade.setIcon(QIcon(resource.get_path('img/app_update.svg'))) self.bt_upgrade.setText(i18n['manage_window.bt.upgrade.text']) self.bt_upgrade.setStyleSheet(toolbar_button_style('#20A435')) self.bt_upgrade.clicked.connect(self.update_selected) self.ref_bt_upgrade = self.toolbar.addWidget(self.bt_upgrade) self.layout.addWidget(self.toolbar) self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache, download_icons=self.download_icons) self.table_apps.change_headers_policy() self.layout.addWidget(self.table_apps) toolbar_console = QToolBar() self.checkbox_console = QCheckBox() self.checkbox_console.setText( self.i18n['manage_window.checkbox.show_details']) self.checkbox_console.stateChanged.connect(self._handle_console) self.checkbox_console.setVisible(False) self.ref_checkbox_console = toolbar_console.addWidget( self.checkbox_console) toolbar_console.addWidget(new_spacer()) self.label_displayed = QLabel() toolbar_console.addWidget(self.label_displayed) self.layout.addWidget(toolbar_console) self.textarea_output = QPlainTextEdit(self) self.textarea_output.resize(self.table_apps.size()) self.textarea_output.setStyleSheet("background: black; color: white;") self.layout.addWidget(self.textarea_output) self.textarea_output.setVisible(False) self.textarea_output.setReadOnly(True) self.toolbar_substatus = QToolBar() self.toolbar_substatus.addWidget(new_spacer()) self.label_substatus = QLabel() self.toolbar_substatus.addWidget(self.label_substatus) self.toolbar_substatus.addWidget(new_spacer()) self.layout.addWidget(self.toolbar_substatus) self._change_label_substatus('') self.thread_update = self._bind_async_action( UpdateSelectedApps(self.manager, self.i18n), finished_call=self._finish_update_selected) self.thread_refresh = self._bind_async_action( RefreshApps(self.manager), finished_call=self._finish_refresh_apps, only_finished=True) self.thread_uninstall = self._bind_async_action( UninstallApp(self.manager, self.icon_cache), finished_call=self._finish_uninstall) self.thread_get_info = self._bind_async_action( GetAppInfo(self.manager), finished_call=self._finish_get_info) self.thread_get_history = self._bind_async_action( GetAppHistory(self.manager, self.i18n), finished_call=self._finish_get_history) self.thread_search = self._bind_async_action( SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True) self.thread_downgrade = self._bind_async_action( DowngradeApp(self.manager, self.i18n), finished_call=self._finish_downgrade) self.thread_suggestions = self._bind_async_action( FindSuggestions(man=self.manager), finished_call=self._finish_search, only_finished=True) self.thread_run_app = self._bind_async_action( LaunchApp(self.manager), finished_call=self._finish_run_app, only_finished=False) self.thread_custom_action = self._bind_async_action( CustomAction(manager=self.manager), finished_call=self._finish_custom_action) self.thread_apply_filters = ApplyFilters() self.thread_apply_filters.signal_finished.connect( self._finish_apply_filters_async) self.thread_apply_filters.signal_table.connect( self._update_table_and_upgrades) self.signal_table_update.connect( self.thread_apply_filters.stop_waiting) self.thread_install = InstallPackage(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, locale_keys=self.i18n) self._bind_async_action(self.thread_install, finished_call=self._finish_install) self.thread_animate_progress = AnimateProgress() self.thread_animate_progress.signal_change.connect( self._update_progress) self.thread_verify_models = VerifyModels() self.thread_verify_models.signal_updates.connect( self._notify_model_data_change) self.toolbar_bottom = QToolBar() self.toolbar_bottom.setIconSize(QSize(16, 16)) self.toolbar_bottom.setStyleSheet('QToolBar { spacing: 3px }') self.toolbar_bottom.addWidget(new_spacer()) self.progress_bar = QProgressBar() self.progress_bar.setMaximumHeight(10 if QApplication.instance().style( ).objectName().lower() == 'windows' else 4) self.progress_bar.setTextVisible(False) self.ref_progress_bar = self.toolbar_bottom.addWidget( self.progress_bar) self.toolbar_bottom.addWidget(new_spacer()) self.combo_styles = StylesComboBox( parent=self, i18n=i18n, show_panel_after_restart=bool(tray_icon)) self.combo_styles.setStyleSheet('QComboBox {font-size: 12px;}') self.ref_combo_styles = self.toolbar_bottom.addWidget( self.combo_styles) bt_settings = IconButton( icon_path=resource.get_path('img/app_settings.svg'), action=self._show_settings_menu, background='#12ABAB', tooltip=self.i18n['manage_window.bt_settings.tooltip']) self.ref_bt_settings = self.toolbar_bottom.addWidget(bt_settings) self.layout.addWidget(self.toolbar_bottom) qt_utils.centralize(self) self.filter_only_apps = True self.type_filter = self.any_type_filter self.filter_updates = False self._maximized = False self.progress_controll_enabled = True self.recent_installation = False self.dialog_about = None self.first_refresh = suggestions self.thread_warnings = ListWarnings(man=manager, locale_keys=i18n) self.thread_warnings.signal_warnings.connect(self._show_warnings) def set_tray_icon(self, tray_icon): self.tray_icon = tray_icon self.combo_styles.show_panel_after_restart = bool(tray_icon) def _update_process_progress(self, val: int): if self.progress_controll_enabled: self.thread_animate_progress.set_progress(val) def apply_filters_async(self): self.label_status.setText(self.i18n['manage_window.status.filtering'] + '...') self.ref_toolbar_search.setVisible(False) if self.ref_input_name_filter.isVisible(): self.input_name_filter.setReadOnly(True) self.thread_apply_filters.filters = self._gen_filters() self.thread_apply_filters.pkgs = self.pkgs_available self.thread_apply_filters.start() def _update_table_and_upgrades(self, pkgs_info: dict): self._update_table(pkgs_info=pkgs_info, signal=True) self.update_bt_upgrade(pkgs_info) def _finish_apply_filters_async(self, success: bool): self.label_status.setText('') self.ref_toolbar_search.setVisible(True) if self.ref_input_name_filter.isVisible(): self.input_name_filter.setReadOnly(False) def _bind_async_action(self, action: AsyncAction, finished_call, only_finished: bool = False) -> AsyncAction: action.signal_finished.connect(finished_call) if not only_finished: action.signal_confirmation.connect(self._ask_confirmation) action.signal_output.connect(self._update_action_output) action.signal_message.connect(self._show_message) action.signal_status.connect(self._change_label_status) action.signal_substatus.connect(self._change_label_substatus) action.signal_progress.connect(self._update_process_progress) self.signal_user_res.connect(action.confirm) return action def _ask_confirmation(self, msg: dict): self.thread_animate_progress.pause() diag = ConfirmationDialog(title=msg['title'], body=msg['body'], locale_keys=self.i18n, components=msg['components'], confirmation_label=msg['confirmation_label'], deny_label=msg['deny_label']) res = diag.is_confirmed() self.thread_animate_progress.animate() self.signal_user_res.emit(res) def _show_message(self, msg: dict): self.thread_animate_progress.pause() dialog.show_message(title=msg['title'], body=msg['body'], type_=msg['type']) self.thread_animate_progress.animate() def _show_warnings(self, warnings: List[str]): if warnings: dialog.show_message(title=self.i18n['warning'].capitalize(), body='<p>{}</p>'.format( '<br/><br/>'.join(warnings)), type_=MessageType.WARNING) def show(self): super(ManageWindow, self).show() if not self.thread_warnings.isFinished(): self.thread_warnings.start() def verify_warnings(self): self.thread_warnings.start() def _show_installed(self): if self.pkgs_installed: self.finish_action() self.ref_bt_upgrade.setVisible(True) self.ref_checkbox_only_apps.setVisible(True) self.input_search.setText('') self.input_name_filter.setText('') self.update_pkgs(new_pkgs=None, as_installed=True) def _show_about(self): if self.dialog_about is None: self.dialog_about = AboutDialog(self.i18n) self.dialog_about.show() def _handle_updates_filter(self, status: int): self.filter_updates = status == 2 self.apply_filters_async() def _handle_filter_only_apps(self, status: int): self.filter_only_apps = status == 2 self.apply_filters_async() def _handle_type_filter(self, idx: int): self.type_filter = self.combo_filter_type.itemData(idx) self.apply_filters_async() def _notify_model_data_change(self): self.table_apps.fill_async_data() def changeEvent(self, e: QEvent): if isinstance(e, QWindowStateChangeEvent): self._maximized = self.isMaximized() policy = QHeaderView.Stretch if self._maximized else QHeaderView.ResizeToContents self.table_apps.change_headers_policy(policy) def closeEvent(self, event): if self.tray_icon: event.ignore() self.hide() self._handle_console_option(False) def _handle_console(self, checked: bool): if checked: self.textarea_output.show() else: self.textarea_output.hide() def _handle_console_option(self, enable: bool): if enable: self.textarea_output.clear() self.ref_checkbox_console.setVisible(enable) self.checkbox_console.setChecked(False) self.textarea_output.hide() def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None): self.recent_installation = False self.type_filter = None self.input_search.clear() if not keep_console: self._handle_console_option(False) self.ref_checkbox_updates.setVisible(False) self.ref_checkbox_only_apps.setVisible(False) self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=True) self.thread_refresh.app = top_app # the app will be on top when refresh happens self.thread_refresh.pkg_types = pkg_types self.thread_refresh.start() def _finish_refresh_apps(self, res: dict, as_installed: bool = True): self.finish_action() self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) self.ref_bt_upgrade.setVisible(True) self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types']) self.first_refresh = False def uninstall_app(self, app: PackageView): pwd = None requires_root = self.manager.requires_root('uninstall', app.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.i18n) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.i18n['manage_window.status.uninstalling'], app.model.name)) self.thread_uninstall.app = app self.thread_uninstall.root_password = pwd self.thread_uninstall.start() def run_app(self, app: PackageView): self._begin_action( self.i18n['manage_window.status.running_app'].format( app.model.name)) self.thread_run_app.app = app self.thread_run_app.start() def _finish_uninstall(self, pkgv: PackageView): self.finish_action() if pkgv: if self._can_notify_user(): util.notify_user('{} ({}) {}'.format(pkgv.model.name, pkgv.model.get_type(), self.i18n['uninstalled'])) self.refresh_apps(pkg_types={pkgv.model.__class__}) else: if self._can_notify_user(): util.notify_user('{}: {}'.format( pkgv.model.name, self.i18n['notification.uninstall.failed'])) self.checkbox_console.setChecked(True) def _can_notify_user(self): return self.notifications and (self.isHidden() or self.isMinimized()) def _finish_downgrade(self, res: dict): self.finish_action() if res['success']: if self._can_notify_user(): util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) self.refresh_apps(pkg_types={res['app'].model.__class__}) if self.tray_icon: self.tray_icon.verify_updates(notify_user=False) else: if self._can_notify_user(): util.notify_user(self.i18n['notification.downgrade.failed']) self.checkbox_console.setChecked(True) def _change_label_status(self, status: str): self.label_status.setText(status) def _change_label_substatus(self, substatus: str): self.label_substatus.setText('<p>{}</p>'.format(substatus)) if not substatus: self.toolbar_substatus.hide() elif not self.toolbar_substatus.isVisible(): self.toolbar_substatus.show() def _update_table(self, pkgs_info: dict, signal: bool = False): self.pkgs = pkgs_info['pkgs_displayed'] self.table_apps.update_pkgs( self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0) if not self._maximized: self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy() self.resize_and_center(accept_lower_width=len(self.pkgs) > 0) self.label_displayed.setText('{} / {}'.format( len(self.pkgs), len(self.pkgs_available))) else: self.label_displayed.setText('') if signal: self.signal_table_update.emit() def update_bt_upgrade(self, pkgs_info: dict = None): show_bt_upgrade = False if not pkgs_info or pkgs_info['not_installed'] == 0: for app_v in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs): if app_v.update_checked: show_bt_upgrade = True break self.ref_bt_upgrade.setVisible(show_bt_upgrade) def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True): self.update_bt_upgrade(pkgs_info) if pkgs_info['updates'] > 0: if pkgs_info['not_installed'] == 0: if not self.ref_checkbox_updates.isVisible(): self.ref_checkbox_updates.setVisible(True) if not self.filter_updates: self._change_checkbox(self.checkbox_updates, True, 'filter_updates', trigger_filters) if pkgs_info['napp_updates'] > 0 and self.filter_only_apps: self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters) else: self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters) self.ref_checkbox_updates.setVisible(False) def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, trigger: bool = True): if not trigger: checkbox.blockSignals(True) checkbox.setChecked(checked) if not trigger: setattr(self, attr, checked) checkbox.blockSignals(False) def _gen_filters(self, updates: int = 0, ignore_updates: bool = False) -> dict: return { 'only_apps': self.filter_only_apps, 'type': self.type_filter, 'updates': False if ignore_updates else self.filter_updates, 'name': self.input_name_filter.get_text().lower() if self.input_name_filter.get_text() else None, 'display_limit': self.display_limit if updates <= 0 else None } def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False): self.input_name_filter.setText('') pkgs_info = commons.new_pkgs_info() filters = self._gen_filters(ignore_updates) if new_pkgs is not None: old_installed = None if as_installed: old_installed = self.pkgs_installed self.pkgs_installed = [] for pkg in new_pkgs: app_model = PackageView(model=pkg) commons.update_info(app_model, pkgs_info) commons.apply_filters(app_model, filters, pkgs_info) if old_installed and types: for pkgv in old_installed: if not pkgv.model.__class__ in types: commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) else: # use installed for pkgv in self.pkgs_installed: commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) if pkgs_info['apps_count'] == 0: if self.first_refresh: self._begin_search('') self.thread_suggestions.start() return else: self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) self.checkbox_only_apps.setCheckable(False) else: self.checkbox_only_apps.setCheckable(True) self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False) self._apply_filters(pkgs_info, ignore_updates=ignore_updates) self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False) self.pkgs_available = pkgs_info['pkgs'] if as_installed: self.pkgs_installed = pkgs_info['pkgs'] self.pkgs = pkgs_info['pkgs_displayed'] if self.pkgs: self.ref_input_name_filter.setVisible(True) self._update_type_filters(pkgs_info['available_types']) self._update_table(pkgs_info=pkgs_info) if new_pkgs: self.thread_verify_models.apps = self.pkgs self.thread_verify_models.start() if self.pkgs_installed: self.ref_bt_installed.setVisible(not as_installed and not self.recent_installation) self.resize_and_center(accept_lower_width=self.pkgs_installed) def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): pkgs_info['pkgs_displayed'] = [] filters = self._gen_filters(updates=pkgs_info['updates'], ignore_updates=ignore_updates) for pkgv in pkgs_info['pkgs']: commons.apply_filters(pkgv, filters, pkgs_info) def _update_type_filters(self, available_types: dict = None): if available_types is None: self.ref_combo_filter_type.setVisible( self.combo_filter_type.count() > 1) else: self.type_filter = self.any_type_filter if available_types and len(available_types) > 1: if self.combo_filter_type.count() > 1: for _ in range(self.combo_filter_type.count() - 1): self.combo_filter_type.removeItem(1) for app_type, icon_path in available_types.items(): icon = self.cache_type_filter_icons.get(app_type) if not icon: icon = load_icon(icon_path, 14) self.cache_type_filter_icons[app_type] = icon self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type) self.ref_combo_filter_type.setVisible(True) else: self.ref_combo_filter_type.setVisible(False) def resize_and_center(self, accept_lower_width: bool = True): if self.pkgs: new_width = reduce(operator.add, [ self.table_apps.columnWidth(i) for i in range(self.table_apps.columnCount()) ]) if self.ref_bt_upgrade.isVisible( ) or self.ref_bt_settings.isVisible(): new_width *= 1.07 else: new_width = self.toolbar_top.width() if accept_lower_width or new_width > self.width(): self.resize(new_width, self.height()) if self.ref_bt_upgrade.isVisible( ) and self.bt_upgrade.visibleRegion().isEmpty(): self.adjustSize() qt_utils.centralize(self) def update_selected(self): if self.pkgs: requires_root = False to_update = [] for app_v in self.pkgs: if app_v.update_checked: to_update.append(app_v) if self.manager.requires_root('update', app_v.model): requires_root = True if to_update and dialog.ask_confirmation( title=self.i18n['manage_window.upgrade_all.popup.title'], body=self.i18n['manage_window.upgrade_all.popup.body'], locale_keys=self.i18n, widgets=[ UpdateToggleButton( None, self, self.i18n, clickable=False) ]): pwd = None if not is_root() and requires_root: pwd, ok = ask_root_password(self.i18n) if not ok: return self._handle_console_option(True) self.progress_controll_enabled = len(to_update) == 1 self._begin_action(self.i18n['manage_window.status.upgrading']) self.thread_update.apps_to_update = to_update self.thread_update.root_password = pwd self.thread_update.start() def _finish_update_selected(self, res: dict): self.finish_action() if res['success']: if self._can_notify_user(): util.notify_user('{} {}'.format( res['updated'], self.i18n['notification.update_selected.success'])) self.refresh_apps(pkg_types=res['types']) if self.tray_icon: self.tray_icon.verify_updates() else: if self._can_notify_user(): util.notify_user( self.i18n['notification.update_selected.failed']) self.ref_bt_upgrade.setVisible(True) self.checkbox_console.setChecked(True) def _update_action_output(self, output: str): self.textarea_output.appendPlainText(output) def _begin_action(self, action_label: str, keep_search: bool = False, keep_bt_installed: bool = True, clear_filters: bool = False): self.ref_input_name_filter.setVisible(False) self.ref_combo_filter_type.setVisible(False) self.ref_bt_settings.setVisible(False) self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.ref_progress_bar.setVisible(True) self.ref_combo_styles.setVisible(False) self.label_status.setText(action_label + "...") self.ref_bt_upgrade.setVisible(False) self.ref_bt_refresh.setVisible(False) self.checkbox_only_apps.setEnabled(False) self.table_apps.setEnabled(False) self.checkbox_updates.setEnabled(False) if not keep_bt_installed: self.ref_bt_installed.setVisible(False) elif self.ref_bt_installed.isVisible(): self.ref_bt_installed.setEnabled(False) if keep_search: self.ref_toolbar_search.setVisible(True) else: self.ref_toolbar_search.setVisible(False) if clear_filters: self._update_type_filters({}) else: self.combo_filter_type.setEnabled(False) def finish_action(self): self.ref_combo_styles.setVisible(True) self.thread_animate_progress.stop = True self.thread_animate_progress.wait() self.ref_progress_bar.setVisible(False) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(False) self._change_label_substatus('') self.ref_bt_settings.setVisible(True) self.ref_bt_refresh.setVisible(True) self.checkbox_only_apps.setEnabled(True) self.table_apps.setEnabled(True) self.input_search.setEnabled(True) self.label_status.setText('') self.label_substatus.setText('') self.ref_toolbar_search.setVisible(True) self.ref_toolbar_search.setEnabled(True) self.combo_filter_type.setEnabled(True) self.checkbox_updates.setEnabled(True) self.progress_controll_enabled = True if self.pkgs: self.ref_input_name_filter.setVisible(True) self.update_bt_upgrade() self._update_type_filters() if self.ref_bt_installed.isVisible(): self.ref_bt_installed.setEnabled(True) def downgrade(self, pkgv: PackageView): pwd = None requires_root = self.manager.requires_root('downgrade', pkgv.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.i18n) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.i18n['manage_window.status.downgrading'], pkgv.model.name)) self.thread_downgrade.app = pkgv self.thread_downgrade.root_password = pwd self.thread_downgrade.start() def get_app_info(self, pkg: dict): self._handle_console_option(False) self._begin_action(self.i18n['manage_window.status.info']) self.thread_get_info.app = pkg self.thread_get_info.start() def get_app_history(self, app: dict): self._handle_console_option(False) self._begin_action(self.i18n['manage_window.status.history']) self.thread_get_history.app = app self.thread_get_history.start() def _finish_get_info(self, app_info: dict): self.finish_action() dialog_info = InfoDialog(app=app_info, icon_cache=self.icon_cache, locale_keys=self.i18n, screen_size=self.screen_size) dialog_info.exec_() def _finish_get_history(self, res: dict): self.finish_action() if res.get('error'): self._handle_console_option(True) self.textarea_output.appendPlainText(res['error']) self.checkbox_console.setChecked(True) else: dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) dialog_history.exec_() def _begin_search(self, word): self._handle_console_option(False) self.ref_checkbox_only_apps.setVisible(False) self.ref_checkbox_updates.setVisible(False) self.filter_updates = False self._begin_action('{} {}'.format( self.i18n['manage_window.status.searching'], word if word else ''), clear_filters=True) def search(self): word = self.input_search.text().strip() if word: self._begin_search(word) self.thread_search.word = word self.thread_search.start() def _finish_search(self, res: dict): self.finish_action() if not res['error']: self.ref_bt_upgrade.setVisible(False) self.update_pkgs(res['pkgs_found'], as_installed=False, ignore_updates=True) else: dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING) def install(self, pkg: PackageView): pwd = None requires_root = self.manager.requires_root('install', pkg.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.i18n) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.i18n['manage_window.status.installing'], pkg.model.name)) self.thread_install.pkg = pkg self.thread_install.root_password = pwd self.thread_install.start() def _finish_install(self, res: dict): self.input_search.setText('') self.finish_action() console_output = self.textarea_output.toPlainText() if console_output: log_path = '/tmp/bauh/logs/install/{}/{}'.format( res['pkg'].model.get_type(), res['pkg'].model.name) try: Path(log_path).mkdir(parents=True, exist_ok=True) log_file = log_path + '/{}.log'.format(int(time.time())) with open(log_file, 'w+') as f: f.write(console_output) self.textarea_output.appendPlainText( self.i18n['console.install_logs.path'].format( '"{}"'.format(log_file))) except: self.textarea_output.appendPlainText( "[warning] Could not write install log file to '{}'". format(log_path)) if res['success']: self.recent_installation = True if self._can_notify_user(): util.notify_user(msg='{} ({}) {}'.format( res['pkg'].model.name, res['pkg'].model.get_type(), self.i18n['installed'])) self._finish_refresh_apps({ 'installed': [res['pkg'].model], 'total': 1, 'types': None }) self.ref_bt_installed.setVisible(False) self.ref_checkbox_only_apps.setVisible(False) else: if self._can_notify_user(): util.notify_user('{}: {}'.format( res['pkg'].model.name, self.i18n['notification.install.failed'])) self.checkbox_console.setChecked(True) def _update_progress(self, value: int): self.progress_bar.setValue(value) def _finish_run_app(self, success: bool): self.finish_action() def execute_custom_action(self, pkg: PackageView, action: PackageAction): pwd = None if not is_root() and action.requires_root: pwd, ok = ask_root_password(self.i18n) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format(self.i18n[action.i18n_status_key], pkg.model.name)) self.thread_custom_action.pkg = pkg self.thread_custom_action.root_password = pwd self.thread_custom_action.custom_action = action self.thread_custom_action.start() def _finish_custom_action(self, res: dict): self.finish_action() if res['success']: self.refresh_apps(pkg_types={res['pkg'].model.__class__}) else: self.checkbox_console.setChecked(True) def show_gems_selector(self): gem_panel = GemSelectorPanel(window=self, manager=self.manager, i18n=self.i18n, config=self.config, show_panel_after_restart=bool( self.tray_icon)) gem_panel.show() def _show_settings_menu(self): menu_row = QMenu() if isinstance(self.manager, GenericSoftwareManager): action_gems = QAction(self.i18n['manage_window.settings.gems']) action_gems.setIcon(self.icon_app) action_gems.triggered.connect(self.show_gems_selector) menu_row.addAction(action_gems) action_about = QAction(self.i18n['manage_window.settings.about']) action_about.setIcon(QIcon(resource.get_path('img/about.svg'))) action_about.triggered.connect(self._show_about) menu_row.addAction(action_about) menu_row.adjustSize() menu_row.popup(QCursor.pos()) menu_row.exec_()
class FullscreenableWidget(QMainWindow): widgetFullscreened = Signal(bool) def __init__(self, parent=None, icon_size=None): QMainWindow.__init__(self, parent) self.toolbar = QToolBar('tools', parent=self) self.toolbar.setContentsMargins(0, 0, 0, 0) self.icon_provider = IconProvider(self) self.dark_mode = self.icon_provider.get_theme_mode(self) if icon_size is not None: self.toolbar.setIconSize(QSize(icon_size, icon_size)) self.addToolBar(Qt.RightToolBarArea, self.toolbar) self.action_fullscreen = QAction(self) self.action_fullscreen.setIcon( QIcon(self.icon_provider.get_icon_path('fullscreen.svg'))) self.toolbar.addAction(self.action_fullscreen) self.action_windowed = QAction(self) self.action_windowed.setIcon( QIcon(self.icon_provider.get_icon_path('fullscreen_exit.svg'))) # signalling: self.action_fullscreen.triggered.connect(self.go_fullscreen) self.action_windowed.triggered.connect(self.go_windowed) self.windowed_parent = None self.win_parent_layout = None self.index_in_layout = None self.windowed_flags = None self.windowed_geometry = None self.position_in_grid = None def go_fullscreen(self): self.windowed_flags = self.windowFlags() self.windowed_geometry = self.geometry() cur_scr = self.screen() # to check which screen s_count = len(qApp.screens()) for i in range(s_count): if qApp.screens()[i] == cur_scr: screen = qApp.screens()[i] if self.parent() is not None: self.windowed_parent = self.parent() self.win_parent_layout = self.windowed_parent.layout() self.index_in_layout = self.win_parent_layout.indexOf(self) if isinstance(self.win_parent_layout, QGridLayout): self.position_in_grid = \ self.win_parent_layout.getItemPosition( self.index_in_layout) self.win_parent_layout.removeWidget(self) self.setParent(None) self.toolbar.insertAction(self.action_fullscreen, self.action_windowed) self.toolbar.removeAction(self.action_fullscreen) self.move(screen.geometry().x(), screen.geometry().y()) self.showFullScreen() self.widgetFullscreened.emit(True) def go_windowed(self): self.showNormal() if self.windowed_parent is not None: if isinstance(self.win_parent_layout, (QBoxLayout, QSplitter)): self.win_parent_layout.insertWidget(self.index_in_layout, self) elif isinstance(self.win_parent_layout, QGridLayout): self.win_parent_layout.addWidget(self, *self.position_in_grid) self.setParent(self.windowed_parent) self.windowed_parent = None self.setGeometry(self.windowed_geometry) self.toolbar.insertAction(self.action_windowed, self.action_fullscreen) self.toolbar.removeAction(self.action_windowed) self.widgetFullscreened.emit(False)
class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.resize(800, 600) MainWindow.setMinimumSize(600, 400) icon = QIcon() icon.addPixmap(QPixmap("icon.png"), QIcon.Normal, QIcon.Off) font = QFont() font.setFamily("MS Sans Serif") font.setBold(True) font.setWeight(75) MainWindow.setWindowIcon(icon) self.centralwidget = QWidget(MainWindow) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menuFiles = QMenu(self.menubar) self.menuMark = QMenu(self.menubar) self.menuCommands = QMenu(self.menubar) self.menuNet = QMenu(self.menubar) self.menuShow = QMenu(self.menubar) self.menuConfiguration = QMenu(self.menubar) self.menuStart = QMenu(self.menubar) MainWindow.setMenuBar(self.menubar) self.helpMenuBar = QMenuBar(self.menubar) self.menuHelp = QMenu(self.helpMenuBar) self.helpMenuBar.addMenu(self.menuHelp) self.menubar.setCornerWidget(self.helpMenuBar) self.toolBar = QToolBar(MainWindow) self.toolBar.setMovable(False) MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) self.toolBar_2 = QToolBar(MainWindow) font2 = QFont() font2.setFamily("MS Sans Serif") font2.setWeight(700) font2.setPixelSize(8) font2.setBold(True) self.model1 = QFileSystemModel() self.model1.setRootPath('C:\\Windows') self.tree1 = QTreeView() self.tree1.setModel(self.model1) self.tree1.setRootIndex(self.model1.index("C:\\Windows\\")) self.tree1.setAnimated(False) self.tree1.setFont(font2) self.tree1.setIndentation(20) self.tree1.setSortingEnabled(True) self.tree1.setItemsExpandable(False) self.tree1.setRootIsDecorated(False) self.tree1.sortByColumn(0, QtCore.Qt.AscendingOrder) self.tree1.resize(350, 480) self.model2 = QFileSystemModel() self.model2.setRootPath('C:\\Windows\\System32') self.tree2 = QTreeView() self.tree2.setModel(self.model2) self.tree2.setRootIndex(self.model2.index("C:\\Windows\\System32\\")) self.tree2.setAnimated(False) self.tree2.setFont(font2) self.tree2.setIndentation(20) self.tree2.setItemsExpandable(False) self.tree2.setRootIsDecorated(False) self.tree2.setSortingEnabled(True) self.tree2.resize(350, 480) for i in range(1, self.tree1.model().columnCount()): self.tree1.header().hideSection(i) for i in range(1, self.tree2.model().columnCount()): self.tree2.header().hideSection(i) self.centralHBox = QHBoxLayout() self.centralHBox.addWidget(self.tree1) self.centralHBox.addWidget(self.tree2) self.centralHBox.setContentsMargins(3, 3, 3, 3) self.centralwidget.setLayout(self.centralHBox) self.toolBar_2.setMovable(False) self.toolBar_3 = QToolBar(MainWindow) self.toolBar_3.setFixedHeight(30) self.toolBar_3.setContentsMargins(1, 1, 1, 1) self.dirLabel = QLabel() self.dirLabel.setText('c:\\Windows>') self.dirBox = QComboBox() self.dirBox.setEditable(True) self.dirBox.addItem('') self.dirBox.setFixedWidth(470) self.spacer = QWidget() self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.toolBar_3.addWidget(self.spacer) self.toolBar_3.addWidget(self.dirLabel) self.toolBar_3.addWidget(self.dirBox) self.toolBar_3.setFont(font) self.toolBar_3.setMovable(False) self.toolBar_3.setStyleSheet("QToolBar{spacing: 5px;}") MainWindow.addToolBar(QtCore.Qt.BottomToolBarArea, self.toolBar_2) MainWindow.addToolBar(QtCore.Qt.BottomToolBarArea, self.toolBar_3) MainWindow.insertToolBarBreak(self.toolBar_3) self.actionIndex = QAction(MainWindow) icon1 = QIcon() icon1.addPixmap(QPixmap("help.png"), QIcon.Normal, QIcon.Off) self.actionIndex.setIcon(icon1) self.actionKeyboard = QAction(MainWindow) self.actionRegistration_Info = QAction(MainWindow) self.actionVisit_Total_CMD_s_website = QAction(MainWindow) self.actionAbout_Total_Commander = QAction(MainWindow) self.actionOption_1 = QAction(MainWindow) self.actionOption_2 = QAction(MainWindow) self.actionOption_3 = QAction(MainWindow) self.actionOption_4 = QAction(MainWindow) self.actionOption_5 = QAction(MainWindow) self.actionOption_6 = QAction(MainWindow) self.actionOption_7 = QAction(MainWindow) self.actionOption_8 = QAction(MainWindow) self.actionOption_9 = QAction(MainWindow) self.actionOption_10 = QAction(MainWindow) self.actionOption_11 = QAction(MainWindow) self.actionOption_12 = QAction(MainWindow) self.actionOption_13 = QAction(MainWindow) self.actionOption_14 = QAction(MainWindow) self.Refresh = QAction(MainWindow) icon2 = QIcon() icon2.addPixmap(QPixmap("refresh.png"), QIcon.Normal, QIcon.Off) self.Refresh.setIcon(icon2) self.action_view1 = QAction(MainWindow) icon3 = QIcon() icon3.addPixmap(QPixmap("folder_structure_1.png"), QIcon.Normal, QIcon.Off) self.view_group = QActionGroup(MainWindow) self.action_view1.setIcon(icon3) self.action_view1.setCheckable(True) self.action_view2 = QAction(MainWindow) self.action_view2.setCheckable(True) self.action_view2.setChecked(True) icon4 = QIcon() icon4.addPixmap(QPixmap("folder_structure_2.png"), QIcon.Normal, QIcon.Off) self.action_view2.setIcon(icon4) self.view_group.addAction(self.action_view1) self.view_group.addAction(self.action_view2) self.view_group.setExclusive(True) self.actionF3_View = QToolButton(MainWindow) self.actionF3_View.setStyleSheet("QToolButton{border: none;}") self.actionF3_View.setFont(font) self.actionF4_Edit = QToolButton(MainWindow) self.actionF4_Edit.setStyleSheet("QToolButton{border: none;}") self.actionF4_Edit.setFont(font) self.actionF5_Copy = QToolButton(MainWindow) self.actionF5_Copy.setStyleSheet("QToolButton{border: none;}") self.actionF5_Copy.setFont(font) self.actionF6_Move = QToolButton(MainWindow) self.actionF6_Move.setStyleSheet("QToolButton{border: none;}") self.actionF6_Move.setFont(font) self.actionF5_NewFolder = QToolButton(MainWindow) self.actionF5_NewFolder.setStyleSheet("QToolButton{border: none;}") self.actionF5_NewFolder.setFont(font) self.actionF8_Delete = QToolButton(MainWindow) self.actionF8_Delete.setStyleSheet("QToolButton{border: none;}") self.actionF8_Delete.setFont(font) self.actionAlt_F4_Exit = QToolButton(MainWindow) self.actionAlt_F4_Exit.setStyleSheet("QToolButton{border: none;}") self.actionAlt_F4_Exit.setFont(font) self.menuFiles.addAction(self.actionOption_1) self.menuFiles.addAction(self.actionOption_2) self.menuMark.addAction(self.actionOption_3) self.menuMark.addAction(self.actionOption_4) self.menuCommands.addAction(self.actionOption_5) self.menuCommands.addAction(self.actionOption_6) self.menuNet.addAction(self.actionOption_7) self.menuNet.addAction(self.actionOption_8) self.menuShow.addAction(self.actionOption_9) self.menuShow.addAction(self.actionOption_10) self.menuConfiguration.addAction(self.actionOption_11) self.menuConfiguration.addAction(self.actionOption_12) self.menuStart.addAction(self.actionOption_13) self.menuStart.addAction(self.actionOption_14) self.menuHelp.addAction(self.actionIndex) self.menuHelp.addAction(self.actionKeyboard) self.menuHelp.addAction(self.actionRegistration_Info) self.menuHelp.addAction(self.actionVisit_Total_CMD_s_website) self.menuHelp.addSeparator() self.menuHelp.addAction(self.actionAbout_Total_Commander) self.menubar.addAction(self.menuFiles.menuAction()) self.menubar.addAction(self.menuMark.menuAction()) self.menubar.addAction(self.menuCommands.menuAction()) self.menubar.addAction(self.menuNet.menuAction()) self.menubar.addAction(self.menuShow.menuAction()) self.menubar.addAction(self.menuConfiguration.menuAction()) self.menubar.addAction(self.menuStart.menuAction()) self.helpMenuBar.addAction(self.menuHelp.menuAction()) self.toolBar.addAction(self.Refresh) self.toolBar.addSeparator() self.toolBar.addAction(self.action_view1) self.toolBar.addSeparator() self.toolBar.addAction(self.action_view2) self.separator1 = QFrame() self.separator1.setFrameShape(QFrame.VLine) self.separator1.setFrameShadow(QFrame.Sunken) self.separator2 = QFrame() self.separator2.setFrameShape(QFrame.VLine) self.separator2.setFrameShadow(QFrame.Sunken) self.separator3 = QFrame() self.separator3.setFrameShape(QFrame.VLine) self.separator3.setFrameShadow(QFrame.Sunken) self.separator4 = QFrame() self.separator4.setFrameShape(QFrame.VLine) self.separator4.setFrameShadow(QFrame.Sunken) self.separator5 = QFrame() self.separator5.setFrameShape(QFrame.VLine) self.separator5.setFrameShadow(QFrame.Sunken) self.separator6 = QFrame() self.separator6.setFrameShape(QFrame.VLine) self.separator6.setFrameShadow(QFrame.Sunken) self.toolbarHBoxWidget = QWidget() self.toolbarHBoxWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.toolbarHBox = QGridLayout() self.toolbarHBoxWidget.setLayout(self.toolbarHBox) self.toolbarHBox.addWidget(self.actionF3_View, 0, 0) self.toolbarHBox.addWidget(self.separator1, 0, 1) self.toolbarHBox.addWidget(self.actionF4_Edit, 0, 2) self.toolbarHBox.addWidget(self.separator2, 0, 3) self.toolbarHBox.addWidget(self.actionF5_Copy, 0, 4) self.toolbarHBox.addWidget(self.separator3, 0, 5) self.toolbarHBox.addWidget(self.actionF6_Move, 0, 6) self.toolbarHBox.addWidget(self.separator4, 0, 7) self.toolbarHBox.addWidget(self.actionF5_NewFolder, 0, 8) self.toolbarHBox.addWidget(self.separator5, 0, 9) self.toolbarHBox.addWidget(self.actionF8_Delete, 0, 10) self.toolbarHBox.addWidget(self.separator6, 0, 11) self.toolbarHBox.addWidget(self.actionAlt_F4_Exit, 0, 12) self.toolBar_2.addWidget(self.toolbarHBoxWidget) self.toolBar_2.setFixedHeight(40) self.retranslateUi(MainWindow) QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle( "Total Commander 7.50a - Politechnika Lodzka - Wydzial EEIA") self.menuFiles.setTitle("Files") self.menuMark.setTitle("Mark") self.menuCommands.setTitle("Commands") self.menuNet.setTitle("Net") self.menuShow.setTitle("Show") self.menuConfiguration.setTitle("Configuration") self.menuStart.setTitle("Start") self.menuHelp.setTitle("Help") self.toolBar.setWindowTitle("toolBar") self.toolBar_2.setWindowTitle("toolBar_2") self.actionIndex.setText("Index") self.actionIndex.setIconText("Index") self.actionIndex.setShortcut("F1") self.actionKeyboard.setText("Keyboard") self.actionRegistration_Info.setText("Registration Info") self.actionVisit_Total_CMD_s_website.setText( "Visit Totalcmd\'s Web Site") self.actionAbout_Total_Commander.setText("About Total Commander...") self.actionOption_1.setText("Option 1") self.actionOption_2.setText("Option 2") self.actionOption_3.setText("Option 1") self.actionOption_4.setText("Option 2") self.actionOption_5.setText("Option 1") self.actionOption_6.setText("Option 2") self.actionOption_7.setText("Option 1") self.actionOption_8.setText("Option 2") self.actionOption_9.setText("Option 1") self.actionOption_10.setText("Option 2") self.actionOption_11.setText("Option 1") self.actionOption_12.setText("Option 2") self.actionOption_13.setText("Option 1") self.actionOption_14.setText("Option 2") self.Refresh.setText("Refresh") self.action_view1.setText("View 1") self.action_view2.setText("View 2") self.actionF3_View.setText("F3 View") self.actionF3_View.setShortcut("F3") self.actionF4_Edit.setText("F4 Edit") self.actionF4_Edit.setShortcut("F4") self.actionF5_Copy.setText("F5 Copy") self.actionF5_Copy.setShortcut("F5") self.actionF6_Move.setText("F6 Move") self.actionF6_Move.setShortcut("F6") self.actionF5_NewFolder.setText("F7 NewFolder") self.actionF5_NewFolder.setShortcut("F7") self.actionF8_Delete.setText("F8 Delete") self.actionF8_Delete.setShortcut("F8") self.actionAlt_F4_Exit.setText("Alt+F4 Exit") self.actionAlt_F4_Exit.setShortcut("Ctrl+Alt+F4")
class LibraryManagerPanel(QDockWidget): def __init__(self, task_executor): super().__init__() self.lib_selector = QComboBox() self.libraries = self._setup_libraries(self.lib_selector) self.lib_selector.currentIndexChanged.connect(self._lib_changed) self.info_ico_lbl = QLabel("ico") self.info_lbl = QLabel("Library Information") self.info_lbl.setStyleSheet('font-size: 8pt;') self.task_executor = task_executor self.group_manager = LibraryGroupingManager() self.treeView = QTreeView() self.toolbar = QToolBar() self.refresh_action = CommonUtils.create_toolbar_action( "Refresh/Rescan this Library", QIcon.fromTheme("view-refresh"), self.refresh) self.edit_action = CommonUtils.create_toolbar_action( "Configure this Library", QIcon.fromTheme("preferences-system"), self.edit) self.delete_action = CommonUtils.create_toolbar_action( "Delete this Library", QIcon.fromTheme("list-remove"), self.delete) self.add_action = CommonUtils.create_toolbar_action( "Add a new Library", QIcon.fromTheme("list-add"), self.add) self._init_ui() def _init_ui(self): h_layout = QHBoxLayout() h_layout.addWidget(self.info_ico_lbl) h_layout.addWidget(self.lib_selector, 1) dummy = QWidget() dummy.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.toolbar.addWidget(dummy) self.toolbar.addActions([ self.refresh_action, self.edit_action, self.delete_action, self.add_action ]) self.toolbar.setContentsMargins(0, 0, 0, 0) self.toolbar.setIconSize(QSize(16, 16)) layout = QVBoxLayout() layout.setContentsMargins(5, 0, 0, 0) layout.setMenuBar(self.toolbar) layout.addLayout(h_layout) layout.addWidget(self.info_lbl) layout.addWidget(self.group_manager) layout.addWidget(self.treeView, 1) container = QWidget() container.setLayout(layout) self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetClosable) self.setWidget(container) self.setWindowTitle("Library Manager") if self.lib_selector.count() > 0: self._lib_changed() def refresh(self): lib = self.libraries[self.lib_selector.currentText()] task_name = f"Refreshing {lib['name']}" task_id = f"{time.time()} {task_name}" self.task_executor.execute_with_progress( LibraryManagement.refresh_library, (lib, task_id), (task_name, task_id)) def delete(self): lib_name = self.lib_selector.currentText() choice = QMessageBox.question( self, "Confirm Library Deletion", f"Are you sure you want to delete {lib_name}. " f"Please note this action cannot be undone") if choice == QMessageBox.Yes: lib = self.libraries.pop(lib_name) self.task_executor.execute(LibraryManagement.delete_library, (lib, )) self.lib_selector.removeItem(self.lib_selector.currentIndex()) def edit(self): library = self.libraries[self.lib_selector.currentText()] dialog = LibraryEditorPanel(library) result = dialog.exec() if result == QDialog.Accepted: self.libraries.pop(self.lib_selector.currentText()) lib_new = dialog.get_library() self.libraries[self.lib_selector.currentText()] = lib_new task_name = f"Adding {lib_new['name']}" task_id = f"{time.time()} {task_name}" self.task_executor.execute(LibraryManagement.update_library, (lib_new, )) self._lib_changed() def add(self): dialog = LibraryEditorPanel() result = dialog.exec() if result == QDialog.Accepted: lib = dialog.get_library() self.libraries[lib['name']] = lib self.lib_selector.addItem(lib['name']) task_name = f"Adding {lib['name']}" task_id = f"{time.time()} {task_name}" self.task_executor.execute_with_progress( LibraryManagement.create_library, (lib['name'], lib['type'], lib['dirs'], None, task_id), (task_name, task_id)) def _lib_changed(self): library = self.libraries[self.lib_selector.currentText()] info_ico = QIcon.fromTheme( LibraryManagement.Library_Types[library["type"]]["icon"]) self.info_ico_lbl.setPixmap(info_ico.pixmap(28, 28)) updated = 'Not updated' if library.__contains__('updated'): updated = f"Updated on {library['updated']}" dir_count = len(library['dirs']) lib_dir_details = f"{dir_count} directories" if dir_count == 1: lib_dir_details = f"{dir_count} directory" self.info_lbl.setText( f"Created on {library['created']}<br>{updated}<br>{lib_dir_details}" ) self.group_manager.update_grouping( LibraryManagement.get_group_keys(library["type"])) @staticmethod def _setup_libraries(lib_combo_selector): libraries = LibraryManagement.get_all_libraries() lib_lookup = {} for lib_type in libraries: for lib in libraries[lib_type]: key = f"[{lib_type}] {lib['name']}" lib_lookup[key] = lib lib_combo_selector.addItem(key) return lib_lookup
class LibraryGroupingManager(CollapsibleWidget): def __init__(self): super().__init__("Library Tree Group Manager") self.source = QListWidget() self.target = QListWidget() self.action_toolbar = QToolBar() self.add_action = CommonUtils.create_toolbar_action( "Add selected key to grouping", QIcon.fromTheme("media-skip-forward"), self.add) self.sub_action = CommonUtils.create_toolbar_action( "Remove selected key to grouping", QIcon.fromTheme("media-skip-backward"), self.sub) self.reset_action = CommonUtils.create_toolbar_action( "Reset to library type default", QIcon.fromTheme("document-revert"), self.reset) self.apply_action = CommonUtils.create_toolbar_action( "Apply this view", QIcon.fromTheme("document-save"), self.save) self.icon = QIcon.fromTheme("folder") self._init_ui() def _init_ui(self): self.action_toolbar.setOrientation(Qt.Vertical) dummy = QWidget() dummy.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.action_toolbar.addWidget(dummy) self.action_toolbar.addActions([ self.add_action, self.sub_action, self.reset_action, self.apply_action ]) self.action_toolbar.setContentsMargins(0, 0, 0, 0) q_size = QSize(16, 16) self.action_toolbar.setIconSize(q_size) self.source.setIconSize(q_size) self.source.setMaximumHeight(120) self.target.setIconSize(q_size) self.target.setMaximumHeight(120) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.source) layout.addWidget(self.action_toolbar) layout.addWidget(self.target) self.set_content_layout(layout) def update_grouping(self, grouping): self.source.clear() for key in grouping: item = QListWidgetItem(self.icon, key) self.source.addItem(item) def add(self): self.swap_items(self.source, self.target) def sub(self): self.swap_items(self.target, self.source) def save(self): pass def reset(self): pass @staticmethod def swap_items(source, target): list_items = source.selectedItems() if not list_items: return for item in list_items: source.takeItem(source.row(item)) target.addItem(item)
class ManageWindow(QWidget): __BASE_HEIGHT__ = 400 def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, tray_icon=None): super(ManageWindow, self).__init__() self.locale_keys = locale_keys self.manager = manager self.tray_icon = tray_icon self.working = False # restrict the number of threaded actions self.apps = [] self.label_flatpak = None self.icon_cache = icon_cache self.disk_cache = disk_cache self.download_icons = download_icons self.screen_size = screen_size self.icon_flathub = QIcon(resource.get_path('img/logo.svg')) self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) self.setWindowIcon(self.icon_flathub) self.layout = QVBoxLayout() self.setLayout(self.layout) self.toolbar_top = QToolBar() self.toolbar_top.addWidget(self._new_spacer()) self.label_status = QLabel() self.label_status.setText('') self.label_status.setStyleSheet("font-weight: bold") self.toolbar_top.addWidget(self.label_status) self.toolbar_search = QToolBar() self.toolbar_search.setStyleSheet("spacing: 0px;") self.toolbar_search.setContentsMargins(0, 0, 0, 0) label_pre_search = QLabel() label_pre_search.setStyleSheet( "background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;" ) self.toolbar_search.addWidget(label_pre_search) self.input_search = QLineEdit() self.input_search.setMaxLength(20) self.input_search.setFrame(False) self.input_search.setPlaceholderText( self.locale_keys['window_manage.input_search.placeholder'] + "...") self.input_search.setToolTip( self.locale_keys['window_manage.input_search.tooltip']) self.input_search.setStyleSheet( "QLineEdit { background-color: white; color: gray; spacing: 0;}") self.input_search.returnPressed.connect(self.search) self.toolbar_search.addWidget(self.input_search) label_pos_search = QLabel() label_pos_search.setPixmap(QPixmap( resource.get_path('img/search.svg'))) label_pos_search.setStyleSheet( "background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;" ) self.toolbar_search.addWidget(label_pos_search) self.ref_toolbar_search = self.toolbar_top.addWidget( self.toolbar_search) self.toolbar_top.addWidget(self._new_spacer()) self.layout.addWidget(self.toolbar_top) toolbar = QToolBar() self.checkbox_updates = QCheckBox() self.checkbox_updates.setText(self.locale_keys['updates'].capitalize()) self.checkbox_updates.stateChanged.connect(self._handle_updates_filter) self.ref_checkbox_updates = toolbar.addWidget(self.checkbox_updates) self.checkbox_only_apps = QCheckBox() self.checkbox_only_apps.setText( self.locale_keys['manage_window.checkbox.only_apps']) self.checkbox_only_apps.setChecked(True) self.checkbox_only_apps.stateChanged.connect( self._handle_filter_only_apps) self.ref_checkbox_only_apps = toolbar.addWidget( self.checkbox_only_apps) self.extra_filters = QWidget() self.extra_filters.setLayout(QHBoxLayout()) toolbar.addWidget(self.extra_filters) toolbar.addWidget(self._new_spacer()) self.bt_refresh = QToolButton() self.bt_refresh.setToolTip( locale_keys['manage_window.bt.refresh.tooltip']) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.clicked.connect( lambda: self.refresh_apps(keep_console=False)) toolbar.addWidget(self.bt_refresh) self.bt_upgrade = QToolButton() self.bt_upgrade.setToolTip( locale_keys['manage_window.bt.upgrade.tooltip']) self.bt_upgrade.setIcon( QIcon(resource.get_path('img/update_green.svg'))) self.bt_upgrade.setEnabled(False) self.bt_upgrade.clicked.connect(self.update_selected) self.ref_bt_upgrade = toolbar.addWidget(self.bt_upgrade) self.layout.addWidget(toolbar) self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache, download_icons=self.download_icons) self.table_apps.change_headers_policy() self.layout.addWidget(self.table_apps) toolbar_console = QToolBar() self.checkbox_console = QCheckBox() self.checkbox_console.setText( self.locale_keys['manage_window.checkbox.show_details']) self.checkbox_console.stateChanged.connect(self._handle_console) self.checkbox_console.setVisible(False) self.ref_checkbox_console = toolbar_console.addWidget( self.checkbox_console) toolbar_console.addWidget(self._new_spacer()) self.layout.addWidget(toolbar_console) self.textarea_output = QPlainTextEdit(self) self.textarea_output.resize(self.table_apps.size()) self.textarea_output.setStyleSheet("background: black; color: white;") self.layout.addWidget(self.textarea_output) self.textarea_output.setVisible(False) self.textarea_output.setReadOnly(True) self.thread_update = UpdateSelectedApps(self.manager) self.thread_update.signal_output.connect(self._update_action_output) self.thread_update.signal_finished.connect( self._finish_update_selected) self.thread_update.signal_status.connect( self._change_updating_app_status) self.thread_refresh = RefreshApps(self.manager) self.thread_refresh.signal.connect(self._finish_refresh_apps) self.thread_uninstall = UninstallApp(self.manager, self.icon_cache) self.thread_uninstall.signal_output.connect(self._update_action_output) self.thread_uninstall.signal_finished.connect(self._finish_uninstall) self.thread_downgrade = DowngradeApp(self.manager, self.locale_keys) self.thread_downgrade.signal_output.connect(self._update_action_output) self.thread_downgrade.signal_finished.connect(self._finish_downgrade) self.thread_get_info = GetAppInfo(self.manager) self.thread_get_info.signal_finished.connect(self._finish_get_info) self.thread_get_history = GetAppHistory(self.manager, self.locale_keys) self.thread_get_history.signal_finished.connect( self._finish_get_history) self.thread_search = SearchApps(self.manager) self.thread_search.signal_finished.connect(self._finish_search) self.thread_install = InstallApp(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, locale_keys=self.locale_keys) self.thread_install.signal_output.connect(self._update_action_output) self.thread_install.signal_finished.connect(self._finish_install) self.thread_animate_progress = AnimateProgress() self.thread_animate_progress.signal_change.connect( self._update_progress) self.thread_verify_models = VerifyModels() self.thread_verify_models.signal_updates.connect( self._notify_model_data_change) self.thread_refresh_app = RefreshApp(manager=self.manager) self.thread_refresh_app.signal_finished.connect(self._finish_refresh) self.thread_refresh_app.signal_output.connect( self._update_action_output) self.thread_suggestions = FindSuggestions(man=self.manager) self.thread_suggestions.signal_finished.connect(self._finish_search) self.toolbar_bottom = QToolBar() self.toolbar_bottom.setIconSize(QSize(16, 16)) self.label_updates = QLabel() self.ref_label_updates = self.toolbar_bottom.addWidget( self.label_updates) self.toolbar_bottom.addWidget(self._new_spacer()) self.progress_bar = QProgressBar() self.progress_bar.setTextVisible(False) self.ref_progress_bar = self.toolbar_bottom.addWidget( self.progress_bar) self.toolbar_bottom.addWidget(self._new_spacer()) bt_about = QToolButton() bt_about.setStyleSheet('QToolButton { border: 0px; }') bt_about.setIcon(QIcon(resource.get_path('img/about.svg'))) bt_about.clicked.connect(self._show_about) bt_about.setToolTip(self.locale_keys['manage_window.bt_about.tooltip']) self.ref_bt_about = self.toolbar_bottom.addWidget(bt_about) self.layout.addWidget(self.toolbar_bottom) self.centralize() self.filter_only_apps = True self.filter_types = set() self.filter_updates = False self._maximized = False self.dialog_about = None self.first_refresh = suggestions self.thread_warnings = ListWarnings(man=manager, locale_keys=locale_keys) self.thread_warnings.signal_warnings.connect(self._show_warnings) def _show_warnings(self, warnings: List[str]): if warnings: for warning in warnings: dialog.show_warning( title=self.locale_keys['warning'].capitalize(), body=warning) def show(self): super(ManageWindow, self).show() if not self.thread_warnings.isFinished(): self.thread_warnings.start() def _show_about(self): if self.dialog_about is None: self.dialog_about = AboutDialog(self.locale_keys) self.dialog_about.show() def _handle_updates_filter(self, status: int): self.filter_updates = status == 2 self.apply_filters() def _handle_filter_only_apps(self, status: int): self.filter_only_apps = status == 2 self.apply_filters() def _handle_type_filter(self, status: int, app_type: str): if status == 2: self.filter_types.add(app_type) elif app_type in self.filter_types: self.filter_types.remove(app_type) self.apply_filters() def _notify_model_data_change(self): self.table_apps.fill_async_data() def _new_spacer(self): spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) return spacer def changeEvent(self, e: QEvent): if isinstance(e, QWindowStateChangeEvent): self._maximized = self.isMaximized() policy = QHeaderView.Stretch if self._maximized else QHeaderView.ResizeToContents self.table_apps.change_headers_policy(policy) def closeEvent(self, event): if self.tray_icon: event.ignore() self.hide() self._handle_console_option(False) def _handle_console(self, checked: bool): if checked: self.textarea_output.show() else: self.textarea_output.hide() def _handle_console_option(self, enable: bool): if enable: self.textarea_output.clear() self.ref_checkbox_console.setVisible(enable) self.checkbox_console.setChecked(False) self.textarea_output.hide() def refresh_apps(self, keep_console: bool = True): self.filter_types.clear() self.input_search.clear() if not keep_console: self._handle_console_option(False) self.ref_checkbox_updates.setVisible(False) self.ref_checkbox_only_apps.setVisible(False) self._begin_action(self.locale_keys['manage_window.status.refreshing'], clear_filters=True) self.thread_refresh.start() def _finish_refresh_apps(self, apps: List[Application]): self.finish_action() self.ref_checkbox_only_apps.setVisible(True) self.ref_bt_upgrade.setVisible(True) self.update_apps(apps) self.first_refresh = False def uninstall_app(self, app: ApplicationView): pwd = None requires_root = self.manager.requires_root('uninstall', app.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.locale_keys) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.locale_keys['manage_window.status.uninstalling'], app.model.base_data.name)) self.thread_uninstall.app = app self.thread_uninstall.root_password = pwd self.thread_uninstall.start() def refresh(self, app: ApplicationView): pwd = None requires_root = self.manager.requires_root('refresh', app.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.locale_keys) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.locale_keys['manage_window.status.refreshing_app'], app.model.base_data.name)) self.thread_refresh_app.app = app self.thread_refresh_app.root_password = pwd self.thread_refresh_app.start() def _finish_uninstall(self, app: ApplicationView): self.finish_action() if app: if self._can_notify_user(): system.notify_user('{} ({}) {}'.format( app.model.base_data.name, app.model.get_type(), self.locale_keys['uninstalled'])) self.refresh_apps() else: if self._can_notify_user(): system.notify_user('{}: {}'.format( app.model.base_data.name, self.locale_keys['notification.uninstall.failed'])) self.checkbox_console.setChecked(True) def _can_notify_user(self): return self.isHidden() or self.isMinimized() def _finish_downgrade(self, success: bool): self.finish_action() if success: if self._can_notify_user(): app = self.table_apps.get_selected_app() system.notify_user('{} ({}) {}'.format( app.model.base_data.name, app.model.get_type(), self.locale_keys['downgraded'])) self.refresh_apps() if self.tray_icon: self.tray_icon.verify_updates(notify_user=False) else: if self._can_notify_user(): system.notify_user( self.locale_keys['notification.downgrade.failed']) self.checkbox_console.setChecked(True) def _finish_refresh(self, success: bool): self.finish_action() if success: self.refresh_apps() else: self.checkbox_console.setChecked(True) def _change_updating_app_status(self, app_name: str): self.label_status.setText('{} {}...'.format( self.locale_keys['manage_window.status.upgrading'], app_name)) def apply_filters(self): if self.apps: visible_apps = len(self.apps) for idx, app_v in enumerate(self.apps): hidden = self.filter_only_apps and app_v.model.is_library() if not hidden and self.filter_types is not None: hidden = app_v.model.get_type() not in self.filter_types if not hidden and self.filter_updates: hidden = not app_v.model.update self.table_apps.setRowHidden(idx, hidden) app_v.visible = not hidden visible_apps -= 1 if hidden else 0 self.change_update_state(change_filters=False) if not self._maximized: self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy() self.resize_and_center(accept_lower_width=visible_apps > 0) def change_update_state(self, change_filters: bool = True): enable_bt_update = False app_updates, library_updates, not_installed = 0, 0, 0 for app_v in self.apps: if app_v.model.update: if app_v.model.runtime: library_updates += 1 else: app_updates += 1 if not app_v.model.installed: not_installed += 1 for app_v in self.apps: if not_installed == 0 and app_v.visible and app_v.update_checked: enable_bt_update = True break self.bt_upgrade.setEnabled(enable_bt_update) total_updates = app_updates + library_updates if total_updates > 0: self.label_updates.setPixmap( QPixmap(resource.get_path('img/exclamation.svg')).scaled( 16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.label_updates.setToolTip('{}: {} ( {} {} | {} {} )'.format( self.locale_keys['manage_window.label.updates'], total_updates, app_updates, self.locale_keys['manage_window.checkbox.only_apps'].lower(), library_updates, self.locale_keys['others'].lower())) if not_installed == 0: if not self.ref_checkbox_updates.isVisible(): self.ref_checkbox_updates.setVisible(True) if change_filters and not self.checkbox_updates.isChecked(): self.checkbox_updates.setChecked(True) if change_filters and library_updates > 0 and self.checkbox_only_apps.isChecked( ): self.checkbox_only_apps.setChecked(False) else: self.checkbox_updates.setChecked(False) self.ref_checkbox_updates.setVisible(False) self.label_updates.setPixmap(QPixmap()) def centralize(self): geo = self.frameGeometry() screen = QApplication.desktop().screenNumber( QApplication.desktop().cursor().pos()) center_point = QApplication.desktop().screenGeometry(screen).center() geo.moveCenter(center_point) self.move(geo.topLeft()) def update_apps(self, apps: List[Application], update_check_enabled: bool = True): self.apps = [] napps = 0 # number of apps (not libraries) available_types = set() if apps: for app in apps: app_model = ApplicationView( model=app, visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked()) available_types.add(app.get_type()) napps += 1 if not app.is_library() else 0 self.apps.append(app_model) if napps == 0: if self.first_refresh: self._begin_search('') self.thread_suggestions.start() return else: self.checkbox_only_apps.setChecked(False) self.checkbox_only_apps.setCheckable(False) else: self.checkbox_only_apps.setCheckable(True) self.checkbox_only_apps.setChecked(True) self._update_type_filters(available_types) self.table_apps.update_apps(self.apps, update_check_enabled=update_check_enabled) self.apply_filters() self.change_update_state() self.resize_and_center() self.thread_verify_models.apps = self.apps self.thread_verify_models.start() def _update_type_filters(self, available_types: Set[str]): self.filter_types = available_types filters_layout = self.extra_filters.layout() for i in reversed(range(filters_layout.count())): filters_layout.itemAt(i).widget().setParent(None) if available_types: for app_type in sorted(list(available_types)): checkbox_app_type = QCheckBox() checkbox_app_type.setChecked(True) checkbox_app_type.setText(app_type.capitalize()) def handle_click(status: int, filter_type: str = app_type): self._handle_type_filter(status, filter_type) checkbox_app_type.stateChanged.connect(handle_click) filters_layout.addWidget(checkbox_app_type) def resize_and_center(self, accept_lower_width: bool = True): new_width = reduce(operator.add, [ self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names)) ]) * 1.05 if accept_lower_width or new_width > self.width(): self.resize(new_width, self.height()) self.centralize() def update_selected(self): if self.apps: to_update = [ app_v for app_v in self.apps if app_v.visible and app_v.update_checked ] if to_update: if dialog.ask_confirmation( title=self. locale_keys['manage_window.upgrade_all.popup.title'], body=self. locale_keys['manage_window.upgrade_all.popup.body'], locale_keys=self.locale_keys): self._handle_console_option(True) self._begin_action( self.locale_keys['manage_window.status.upgrading']) self.thread_update.apps_to_update = to_update self.thread_update.start() def _finish_update_selected(self, success: bool, updated: int): self.finish_action() if success: if self._can_notify_user(): system.notify_user('{} {}'.format( updated, self.locale_keys['notification.update_selected.success'])) self.refresh_apps() if self.tray_icon: self.tray_icon.verify_updates() else: if self._can_notify_user(): system.notify_user( self.locale_keys['notification.update_selected.failed']) self.bt_upgrade.setEnabled(True) self.checkbox_console.setChecked(True) def _update_action_output(self, output: str): self.textarea_output.appendPlainText(output) def _begin_action(self, action_label: str, keep_search: bool = False, clear_filters: bool = False): self.ref_bt_about.setVisible(False) self.ref_label_updates.setVisible(False) self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.ref_progress_bar.setVisible(True) self.label_status.setText(action_label + "...") self.bt_upgrade.setEnabled(False) self.bt_refresh.setEnabled(False) self.checkbox_only_apps.setEnabled(False) self.table_apps.setEnabled(False) self.checkbox_updates.setEnabled(False) if keep_search: self.ref_toolbar_search.setVisible(True) else: self.ref_toolbar_search.setVisible(False) if clear_filters: self._update_type_filters(set()) else: self.extra_filters.setEnabled(False) def finish_action(self): self.ref_bt_about.setVisible(True) self.ref_progress_bar.setVisible(False) self.ref_label_updates.setVisible(True) self.thread_animate_progress.stop = True self.progress_bar.setValue(0) self.bt_refresh.setEnabled(True) self.checkbox_only_apps.setEnabled(True) self.table_apps.setEnabled(True) self.input_search.setEnabled(True) self.label_status.setText('') self.ref_toolbar_search.setVisible(True) self.ref_toolbar_search.setEnabled(True) self.extra_filters.setEnabled(True) self.checkbox_updates.setEnabled(True) def downgrade_app(self, app: ApplicationView): pwd = None requires_root = self.manager.requires_root( 'downgrade', self.table_apps.get_selected_app().model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.locale_keys) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name)) self.thread_downgrade.app = app self.thread_downgrade.root_password = pwd self.thread_downgrade.start() def get_app_info(self, app: dict): self._handle_console_option(False) self._begin_action(self.locale_keys['manage_window.status.info']) self.thread_get_info.app = app self.thread_get_info.start() def get_app_history(self, app: dict): self._handle_console_option(False) self._begin_action(self.locale_keys['manage_window.status.history']) self.thread_get_history.app = app self.thread_get_history.start() def _finish_get_info(self, app_info: dict): self.finish_action() self.change_update_state(change_filters=False) dialog_info = InfoDialog(app=app_info, icon_cache=self.icon_cache, locale_keys=self.locale_keys, screen_size=self.screen_size) dialog_info.exec_() def _finish_get_history(self, app: dict): self.finish_action() self.change_update_state(change_filters=False) if app.get('error'): self._handle_console_option(True) self.textarea_output.appendPlainText(app['error']) self.checkbox_console.setChecked(True) else: dialog_history = HistoryDialog(app, self.icon_cache, self.locale_keys) dialog_history.exec_() def _begin_search(self, word): self._handle_console_option(False) self.ref_checkbox_only_apps.setVisible(False) self.ref_checkbox_updates.setVisible(False) self.filter_updates = False self._begin_action('{}{}'.format( self.locale_keys['manage_window.status.searching'], '"{}"'.format(word) if word else ''), clear_filters=True) def search(self): word = self.input_search.text().strip() if word: self._begin_search(word) self.thread_search.word = word self.thread_search.start() def _finish_search(self, apps_found: List[Application]): self.finish_action() self.ref_bt_upgrade.setVisible(False) self.update_apps(apps_found, update_check_enabled=False) def install_app(self, app: ApplicationView): pwd = None requires_root = self.manager.requires_root('install', app.model) if not is_root() and requires_root: pwd, ok = ask_root_password(self.locale_keys) if not ok: return self._handle_console_option(True) self._begin_action('{} {}'.format( self.locale_keys['manage_window.status.installing'], app.model.base_data.name)) self.thread_install.app = app self.thread_install.root_password = pwd self.thread_install.start() def _finish_install(self, app: ApplicationView): self.input_search.setText('') self.finish_action() if app: if self._can_notify_user(): system.notify_user('{} ({}) {}'.format( app.model.base_data.name, app.model.get_type(), self.locale_keys['installed'])) self.refresh_apps() else: if self._can_notify_user(): system.notify_user('{}: {}'.format( app.model.base_data.name, self.locale_keys['notification.install.failed'])) self.checkbox_console.setChecked(True) def _update_progress(self, value: int): self.progress_bar.setValue(value)
class Ui_MainWindow(object): def __init__(self): super(Ui_MainWindow, self).__init__() setting_file = open('../settings/settings.json', 'r') self.json_settings = json.load(setting_file) setting_file.close() def setupUi(self, MainWindow): self.mainwindow = MainWindow MainWindow.setObjectName("MainWindow") MainWindow.resize(1600, 900) MainWindow.setWindowIcon(QIcon('../images/logo.png')) MainWindow.setWindowTitle('网络文章下载与存储系统') self.centralwidget = QtWidgets.QWidget(MainWindow) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) self.centralwidget.setSizePolicy(sizePolicy) self.centralwidget.setObjectName("centralwidget") self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") '''菜单栏部分''' self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 992, 26)) self.menubar.setObjectName("menubar") self.menubar.setStyleSheet(QSS.QMenuBar) self.localfile = QtWidgets.QMenu(self.menubar) self.localfile.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.localfile.setToolTipDuration(5) self.localfile.setToolTipsVisible(False) self.localfile.setObjectName("localfile") self.localfile_action = QAction(self.mainwindow) self.localfile_action.setCheckable(False) self.localfile_action.setObjectName('localFileAction') self.localfile_action.triggered.connect(self.openLocalFile) self.localfile_action.setText('本地文件') self.usagespace = QtWidgets.QMenu(self.menubar) self.usagespace.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.usagespace.setToolTipsVisible(True) self.usagespace.setObjectName("usagespace") self.usagespace_action = QAction(self.mainwindow) self.usagespace_action.setCheckable(False) self.usagespace_action.setObjectName('usageSpaceAction') self.usagespace_action.triggered.connect(self.spaceUsage) self.usagespace_action.setText('占用空间') self.settings = QtWidgets.QMenu(self.menubar) self.settings.setToolTipsVisible(True) self.settings.setObjectName("settings") self.settings_action = QAction(self.mainwindow) self.settings_action.setCheckable(False) self.settings_action.setObjectName('settingAction') self.settings_action.triggered.connect(self.showEnvWindow) self.settings_action.setText('存储设置') self.about = QtWidgets.QMenu(self.menubar) self.about.setToolTipsVisible(True) self.about.setObjectName("about") self.about_action = QAction(self.mainwindow) self.about_action.setCheckable(False) self.about_action.setObjectName('aboutAction') self.about_action.triggered.connect(self.showAboutWindow) self.about_action.setText('关于') self.menubar.addAction(self.localfile_action) self.menubar.addAction(self.usagespace_action) self.menubar.addAction(self.settings_action) self.menubar.addAction(self.about_action) MainWindow.setMenuBar(self.menubar) '''工具栏部分''' self.toolBar = QToolBar(MainWindow) self.toolBar.setObjectName("toolBar") self.toolBar.setContentsMargins(QMargins(20, 0, 20, 0)) MainWindow.addToolBar(Qt.TopToolBarArea, self.toolBar) self.refresh_button = QPushButton() self.refresh_button.setText('刷新') refresh_icon = QIcon() refresh_icon.addPixmap(QPixmap('../images/refresh.png'), QIcon.Normal, QIcon.Off) self.refresh_button.setIcon(refresh_icon) self.refresh_button.setIconSize(QSize(25, 25)) self.refresh_button.setStyleSheet(QSS.ButtonStyle) self.refresh_button.clicked.connect(self.refresh_treewidget) self.search_edit = QLineEdit() self.search_edit.setMaximumWidth(200) self.search_edit.setFixedHeight(30) self.search_edit.setPlaceholderText('输入搜索内容') self.search_edit.setStyleSheet(QSS.LineEdit) self.toolBar.addWidget(self.search_edit) self.search_button = QPushButton() self.search_button.setText('搜索') search_icon = QIcon() search_icon.addPixmap(QPixmap('../images/search.png'), QIcon.Normal, QIcon.Off) self.search_button.setIcon(search_icon) self.search_button.setIconSize(QSize(25, 25)) self.search_button.setStyleSheet(QSS.ButtonStyle) self.search_button.clicked.connect(self._search) '''放大按钮''' self.zoom_in_button = QPushButton(self.mainwindow) self.zoom_in_button.setText('放大') zoom_in_icon = QIcon() zoom_in_icon.addPixmap(QPixmap('../images/zoom_in.png'), QIcon.Normal, QIcon.Off) self.zoom_in_button.setIcon(zoom_in_icon) self.zoom_in_button.setIconSize(QSize(25, 25)) self.zoom_in_button.setStyleSheet(QSS.ButtonStyle) self.sp = QDoubleSpinBox(self.mainwindow) self.sp.setRange(1,4.95) self.sp.setValue(1.2) self.sp.setSingleStep(0.1) self.sp.setMinimum(1) self.sp.setMinimumHeight(35) self.sp.setMinimumWidth(60) self.sp.valueChanged.connect(self.sp_value_change) '''缩小按钮''' self.zoom_out_button = QPushButton(self.mainwindow) self.zoom_out_button.setText('缩小') zoom_out_icon = QIcon() zoom_out_icon.addPixmap(QPixmap('../images/zoom_out.png'), QIcon.Normal, QIcon.Off) self.zoom_out_button.setIcon(zoom_out_icon) self.zoom_out_button.setIconSize(QSize(25, 25)) self.zoom_out_button.setStyleSheet(QSS.ButtonStyle) '''弹出页面url按钮''' self.url_button = QPushButton() self.url_button.setText('URL') url_icon = QIcon() url_icon.addPixmap(QPixmap('../images/url.png'), QIcon.Normal, QIcon.Off) self.url_button.setIcon(url_icon) self.url_button.setIconSize(QSize(25, 25)) self.url_button.setStyleSheet(QSS.ButtonStyle) self.back_button = QPushButton() self.back_button.setText('上级') back_icon = QIcon() back_icon.addPixmap(QPixmap('../images/back.png'), QIcon.Normal, QIcon.Off) self.back_button.setIcon(back_icon) self.back_button.setIconSize(QSize(25, 25)) self.back_button.setStyleSheet(QSS.ButtonStyle) self.back_button.clicked.connect(self.file_back) self.toolBar.addWidget(self.search_button) self.toolBar.addWidget(self.refresh_button) self.toolBar.addWidget(self.zoom_in_button) self.toolBar.addWidget(self.sp) self.toolBar.addWidget(self.zoom_out_button) self.toolBar.addWidget(self.url_button) self.toolBar.addWidget(self.back_button) '''可调节伸缩区域''' self.splitter = QtWidgets.QSplitter(self.centralwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.splitter.sizePolicy().hasHeightForWidth()) self.splitter.setSizePolicy(sizePolicy) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") '''左侧''' self.treeWidget_2 = QtWidgets.QTreeWidget(self.splitter) self.treeWidget_2.setStyleSheet(QSS.QTreeView) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.treeWidget_2.sizePolicy().hasHeightForWidth()) self.treeWidget_2.setSizePolicy(sizePolicy) self.treeWidget_2.setMinimumSize(QtCore.QSize(300, 0)) self.treeWidget_2.setObjectName("treeWidget_2") self.treeWidget_2.setHeaderLabels(['文件名', '文件路径']) self.treeWidget_2.setHeaderHidden(True) self.treeWidget_2.setColumnHidden(1, True) self.root = QTreeWidgetItem(self.treeWidget_2) self.json_settings['FILE_LOCATION'] = self.json_settings['FILE_LOCATION'].replace('\\', '/') self.root.setText(0, self.json_settings['FILE_LOCATION'].split('/')[-1]) self.root.setText(1, self.json_settings['FILE_LOCATION']) self.root.setIcon(0, QIcon('../images/folder.png')) self.file_thread = FileListThread(self.root, self.json_settings['FILE_LOCATION']) self.file_thread.start() self.file_thread.sinOut.connect(self.getTreeRoot) '''右侧文件管理''' self.tabWidget = QtWidgets.QTabWidget(self.splitter) self.tabWidget.setStyleSheet(QSS.QTabWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth()) self.tabWidget.setSizePolicy(sizePolicy) self.tabWidget.setObjectName("tabWidget") self.tab_3 = QtWidgets.QWidget() self.tab_3.setObjectName("tab_3") self.table_widget = self.get_file_list(self.json_settings['FILE_LOCATION']) self.paths = [self.json_settings['FILE_LOCATION'],] self.tab3_layout = QVBoxLayout(self.tab_3) self.tab3_layout.addWidget(self.tablewidget) self.tabWidget.addTab(self.tab_3, "文件管理") '''右侧文件预览''' self.tab_4 = QtWidgets.QWidget() self.tab_4.setStyleSheet(''' background:transparent; ''') self.tab_4.setObjectName("tab_4") self.tabWidget.addTab(self.tab_4, "文件预览") self.browser = Browser(self.tab_4) self.zoom_in_button.clicked.connect(self.zoom_in_func) # 放大与缩小按钮出发事件设置 self.zoom_out_button.clicked.connect(self.zoom_out_func) self.sp.setValue(self.browser.zoomFactor()) self.tab_layout = QVBoxLayout(self.tab_4) self.browser.setMinimumSize(QSize(400, 200)) self.tab_layout.addWidget(self.browser) self.browser.setHtml(''' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style type="text/css"> * { padding: 0; margin: 0; } div { padding: 4px 48px; } body { background: #fff; font-family: "微软雅黑"; color: #333; } h1 { font-size: 100px; font-weight: normal; margin-bottom: 12px; } p { line-height: 1.8em; font-size: 36px } </style> </head> <div style="padding: 24px 48px;"> <h1>:)</h1> <p><b>欢迎使用!</b></p> <br> <hr> <br> <ul> <li><b>图片 JPG格式</b></li> <li><b>网页文件 HTML格式</b></li> <li><b>记事本 TXT格式</b></l> </ul> </div> </html> ''') self.browser.show() self.tabWidget.setCurrentIndex(1) self.horizontalLayout.addWidget(self.splitter) self.horizontalLayout_2.addLayout(self.horizontalLayout) MainWindow.setCentralWidget(self.centralwidget) '''状态栏部分''' self.statusBar = QtWidgets.QStatusBar(MainWindow) self.statusBar.setObjectName("statusBar") self.statusBar.showMessage('程序就绪!',msecs=5000) self.statusBar.setStyleSheet(QSS.QStatusBar) MainWindow.setStatusBar(self.statusBar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate def openLocalFile(self): ''' 菜单栏打开本地文件方法 :return: ''' local_path = self.json_settings['FILE_LOCATION'] if '/' in local_path: local_path = local_path.replace('/', '\\') os.system("explorer.exe %s" % os.path.dirname(local_path)) def spaceUsage(self): ''' 菜单栏占用空间方法 :return: ''' local_path = self.json_settings['FILE_LOCATION'] if '/' in local_path: local_path = local_path.replace('/', '\\') if os.path.isdir(local_path): # 文件夹大小需要遍历文件夹内容 然后求和 file_size = self.getdirsize(local_path) else: file_size = os.path.getsize(local_path) file_size = self.approximateSize(file_size) QMessageBox.about(self.mainwindow, '占用空间', local_path + ':' + file_size) def showEnvWindow(self): pass env_dialog = EnvDialog() env_window = QMainWindow(MainWindow) env_dialog.setupUi(env_window) env_window.setWindowModality(Qt.ApplicationModal) env_window.show() def showAboutWindow(self): ''' 打开关于窗口方法 :return: None ''' aboutdialog = AboutDialog() aboutdialog_window = QMainWindow(MainWindow) aboutdialog.setupUi(aboutdialog_window) aboutdialog_window.setWindowModality(Qt.ApplicationModal) aboutdialog_window.show() '''文件存储空间单位转换方法''' def approximateSize(self, size, a_kilobyte_is_1024_bytes=True): '''Convert a file size to human-readable form. Keyword arguments: size -- file size in bytes a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024 if False, use multiples of 1000 Returns: string ''' SUFFIXES = {1024: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1000: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']} if size < 0: raise ValueError('number must be non-negative') multiple = 1024 if a_kilobyte_is_1024_bytes else 1000 for suffix in SUFFIXES[multiple]: size /= multiple if size < multiple: return '{0:.1f} {1}'.format(size, suffix) def browser_finish(self): self.statusBar.showMessage('加载成功',5000) '''搜索按钮点击事件函数''' def _search(self): self.search_name = self.search_edit.text() # print(self.search_name) if not self.search_name: pass else: self.search_res = [] # 搜索结果 self.recursion_search(self.search_name,self.json_settings['FILE_LOCATION'],self.search_res) # print(self.search_res) self.tablewidget.clear() self.tablewidget.setHorizontalHeaderLabels(['文件名','最后修改日期','文件类型','大小','位置']) # self.tablewidget.setColumnHidden(4,False) if not self.search_res: #搜索结果为空 item = QTableWidgetItem('什么都没有搜索到!') self.table_widget.setItem(0,0,item) self.table_widget.setDisabled(True) else: self.tablewidget.setDisabled(False) self.tablewidget.setDisabled(False) for num in range(len(self.search_res)): res = self.search_res[num] res = res.replace('\\','/') if os.path.isdir(res): # 添加文件夹 file_name = res.split('/')[-1] file_pos = os.path.dirname(res) file_size = self.approximateSize(self.getdirsize(res)) last_update_time = self.time_format(os.stat(res).st_mtime) # 最后修改时间 file_type = '文件夹' file_info = [file_name,last_update_time,file_type,file_size,file_pos] else: # 文件类型的结果 file_name = res.split('/')[-1] file_pos = os.path.dirname(res) file_size = self.approximateSize(os.stat(res).st_size) last_update_time = self.time_format(os.stat(res).st_mtime) if file_name.endswith('.html'): file_type = '网页文件(*.html)' elif file_name.endswith('.png'): file_type = '图片文件(*.png)' elif file_name.endswith('.jpg'): file_type = '图片文件(*.jpg)' elif file_name.endswith('.xlsx'): file_type = '数据表格文件(*.xlsx)' elif file_name.endswith('.txt'): file_type = '图片文件(*.txt)' else: file_type = '未知文件类型(*.*)' file_info = [file_name,last_update_time,file_type,file_size,file_pos] for i in range(5): item = QTableWidgetItem(file_info[i]) self.table_widget.setItem(num,i,item) self.tabWidget.setCurrentIndex(0) def recursion_search(self,search_name,search_path,res=[]): for filename in os.listdir(search_path): temp_path = os.path.join(search_path,filename) if os.path.isdir(temp_path): if search_name in filename: res.append(temp_path) else: self.recursion_search(search_name,temp_path,res) else: if search_name in filename: res.append(temp_path) else: return res '''获取文件加载子线程返回的root节点''' def refresh_treewidget(self): self.updateFileTree() def getTreeRoot(self, root): ''' 文件树添加item方法 :param root: :return: None ''' self.file_thread.wait() self.treeWidget_2.addTopLevelItem(root) self.treeWidget_2.doubleClicked.connect(self.fileItemDoubleClick) self.treeWidget_2.setContextMenuPolicy(Qt.CustomContextMenu) self.treeWidget_2.customContextMenuRequested.connect(self.fileTreeCustomRightMenu) def fileItemDoubleClick(self): '''文件树双击事件''' item = self.treeWidget_2.currentItem() file_path = item.text(1) file_path = file_path.replace('\\', '/') # print(file_path) if os.path.isdir(file_path): self.paths.append(file_path) # self.tab3_layout.removeWidget(self.tablewidget) self.tablewidget.clear() self.get_file_list(file_path,self.tablewidget) self.tablewidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tablewidget.customContextMenuRequested['QPoint'].connect(self.tablewidget_right_menu) self.tabWidget.setCurrentIndex(0) else: if file_path.endswith('.html') or file_path.endswith('.jpg') or file_path.endswith('.png') or file_path.endswith('.txt'): file_url = file_path.replace('\\', '/') if file_path.endswith('.html'): f = open(file_url,'r',encoding='utf-8') html = f.read() self.browser.stop() self.browser.setHtml(html) else: self.browser.stop() # self.browser.load(QUrl('file:///' + file_url)) self.browser.load(QUrl('file:///' + file_url)) self.browser.show() self.zoom_in_button.clicked.connect(self.zoom_in_func) # 放大与缩小按钮出发事件设置 self.zoom_out_button.clicked.connect(self.zoom_out_func) self.sp.setValue(self.browser.zoomFactor()) filename = file_url.split('/')[-1] self.tabWidget.setCurrentIndex(1) self.statusBar.showMessage(f'预览文件 -> {filename} ') else: reply = QMessageBox.information(self.mainwindow, "调用系统软件", "软件不支持此类型文件打开!\n是否调用系统程序打开此文件?", QMessageBox.Yes | QMessageBox.No) if reply == 16384: os.startfile(file_path) def fileTreeCustomRightMenu(self, pos): '''文件树右键菜单''' item = self.treeWidget_2.currentItem() file_path = item.text(1) menu = QMenu(self.treeWidget_2) delete = menu.addAction('删除') copy = menu.addAction('复制') paste = menu.addAction('粘贴') openLocalFile = menu.addAction('浏览本地文件') file_roperty = menu.addAction("属性") action = menu.exec_(self.treeWidget_2.mapToGlobal(pos)) if action == delete: reply = QMessageBox.warning(self.mainwindow, '删除确认', '确认删除吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if os.path.isdir(file_path) and reply == 16384: # print('delete dir') shutil.rmtree(file_path) self.statusBar.showMessage(f'删除 {file_path} 成功!') if file_path == self.paths[-1]: # 删除的是当前文件管理显示的文件夹 需要推倒上一级 current_path = self.paths.pop() self.paths.append(os.path.dirname(current_path)) self.updateFileTree() elif not os.path.isdir(file_path) and reply == 16384: # print('delete file') os.remove(file_path) self.statusBar.showMessage("删除 -> %s 成功!" % file_path) self.updateFileTree() self.statusBar.showMessage(f'删除 {file_path} 成功!') elif action == copy: try: data = QMimeData() url = QUrl.fromLocalFile(file_path) clipboard = QApplication.clipboard() data.setUrls([url]) clipboard.setMimeData(data) self.statusBar.showMessage("已复制 -> %s 到剪切板" % file_path) except Exception as e: QMessageBox.about(self.mainwindow, '错误', '文件不存在!') self.statusBar.showMessage("复制 -> %s 出错,文件不存在!" % file_path) elif action == paste: data = QApplication.clipboard().mimeData() try: source_file_url = data.urls()[0].url() self.paste_thread = FilePasteThread(source_file_url[8:], file_path) self.paste_thread.sinOut.connect(self.filePasteComplete) self.paste_thread.start() except IndexError as e: self.statusBar.showMessage('剪切板为空,不能粘贴') elif action == openLocalFile: try: local_path = file_path.replace('/', '\\') os.system("explorer.exe %s" % os.path.dirname(local_path)) except Exception as e: QMessageBox.warning(self.mainwindow, '错误', '打开文件不存在!') elif action == file_roperty: # print('查看文件属性') if os.path.isdir(file_path): file_type = '文件夹' file_image = '../images/folder_status.png' _dir = True else: _dir = False if file_path.endswith('.jpg'): file_type = 'JPG图片文件( *.jpg )' file_image = '../images/jpg_status.png' elif file_path.endswith('.html'): file_type = 'HTML页面文件( *.html )' file_image = '../images/html_status.png' elif file_path.endswith('.xlsx'): file_type = 'XLSX表格文件( *.xlsx )' file_image = '../images/excel_status.png' elif file_path.endswith('.png'): file_type = 'PNG图片文件( *.png )' file_image = '../images/png_status.png' elif file_path.endswith('.txt'): file_type = 'TXT图片文件( *.txt )' file_image = '../images/txt_status.png' else: file_type = 'Other其他文件类型( *.%s)' % (os.path.splitext(file_path)[1]) file_image = '../images/file_status.png' if _dir: '''文件夹大小去要遍历每个子文件夹与文件累加''' file_size = self.getdirsize(file_path) # print(file_path) statinfo = os.stat(file_path) else: statinfo = os.stat(file_path) file_size = statinfo.st_size file_atime = self.time_format(statinfo.st_atime) # 文件最后访问时间 file_ctime = self.time_format(statinfo.st_ctime) # 文件创建时间 file_mtime = self.time_format(statinfo.st_mtime) # 文件最后修改时间 self.file_status_window = FileStatusWindow() self.file_status_window.filename = file_path.replace('\\', '/').split('/')[-1] self.status_main_window = QMainWindow(MainWindow) self.file_status_window.setupUi(self.status_main_window) self.file_status_window.lineEdit.setText(self.file_status_window.filename) self.file_status_window.label_3.setText(file_type) self.file_status_window.label_5.setText(file_path.replace('/', '\\')) self.file_status_window.label_9.setText(file_ctime) self.file_status_window.label_11.setText(file_mtime) self.file_status_window.label_13.setText(file_atime) self.file_status_window.label_7.setText(self.approximateSize(file_size)) self.file_status_window.pushButton.clicked.connect(self.fileStatusUse) # 应用按钮click出发函数 self.file_status_window.pushButton_2.clicked.connect(self.fileStatusConfirm) self.file_status_window.pushButton_3.clicked.connect(self.fileStatusCancel) pix = QPixmap(file_image) self.file_status_window.label.setPixmap(pix) self.status_main_window.show() '''文件粘贴完毕后执行方法''' def filePasteComplete(self, msg): ''' 文件粘贴成功回调函数 成功后刷新重构文件树 :param msg: :return: None ''' self.paste_thread.wait() self.statusBar.showMessage("粘贴文件 -> %s 成功!" % msg) self.updateFileTree() '''更新文件树方法''' def updateFileTree(self): self.treeWidget_2.clear() self.root = QTreeWidgetItem(self.treeWidget_2) self.root.setText(0, self.json_settings['FILE_LOCATION'].split('\\')[-1]) self.root.setText(1, self.json_settings['FILE_LOCATION']) self.root.setIcon(0, QIcon('../images/folder.png')) # self.tab3_layout.removeWidget(self.tablewidget) self.tablewidget.clear() self.get_file_list(self.paths[-1],self.tablewidget) self.file_thread = FileListThread(self.root, self.json_settings['FILE_LOCATION']) self.file_thread.start() self.file_thread.sinOut.connect(self.getTreeRoot) def getdirsize(self, dir_path): '''获取文件夹大小方法''' size = 0 for root, dirs, files in os.walk(dir_path): size += sum([getsize(join(root, name)) for name in files]) return size def time_format(self, timestamp): '''时间格式化方法''' time_array = time.localtime(timestamp) week = { '0': '星期日', '1': '星期一', '2': '星期二', '3': '星期三', '4': '星期四', '5': '星期五', '6': '星期六' } if time_array.tm_mon < 10: tm_mon = '0'+str(time_array.tm_mon) else: tm_mon = time_array.tm_mon if time_array.tm_mday < 10: tm_mday = '0' + str(time_array.tm_mday) else: tm_mday = time_array.tm_mday if time_array.tm_hour < 10: tm_hour = '0'+ str(time_array.tm_hour) else: tm_hour = time_array.tm_hour if time_array.tm_min < 10: tm_min = '0'+ str(time_array.tm_min) else: tm_min = time_array.tm_min if time_array.tm_sec < 10: tm_sec = '0'+ str(time_array.tm_sec) else: tm_sec = time_array.tm_sec return f'{time_array.tm_year}年 {tm_mon}月 {tm_mday}日, {week[str(time_array.tm_wday)]}, {tm_hour}:{tm_min}:{tm_sec}' def fileStatusConfirm(self): self.status_main_window.close() def fileStatusUse(self): status_filename = self.file_status_window.lineEdit.text() if status_filename != self.file_status_window.filename: # print('修改文件名') old_file_path = self.file_status_window.label_5.text() # print(old_file_path) new_fila_path = '\\'.join(old_file_path.split('\\')[:-1]) + '\\' + status_filename os.rename(old_file_path, new_fila_path) self.statusBar.showMessage('重命名文件 -> %s' % new_fila_path) self.file_status_window.pushButton.setEnabled(False) self.updateFileTree() def fileStatusCancel(self): self.status_main_window.close() def zoom_in_func(self): self.browser.setZoomFactor(self.browser.zoomFactor() + 0.3) self.sp.setValue(self.browser.zoomFactor()) def zoom_out_func(self): if self.browser.zoomFactor() > 1: self.browser.setZoomFactor(self.browser.zoomFactor() - 0.2) self.sp.setValue(self.browser.zoomFactor()) def sp_value_change(self): self.browser.setZoomFactor(self.sp.value()) def get_file_list(self,file_path,tablewidget=None): file_list = os.listdir(file_path) # print(file_list) rows = len(file_list) # print(rows) if not tablewidget: self.tablewidget = QTableWidget() self.tablewidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tablewidget.horizontalHeader().setStretchLastSection(True)# 所有列自动拉伸,充满界面 self.tablewidget.setRowCount(rows) self.tablewidget.setColumnCount(5) # self.tablewidget.setColumnHidden(4,True) self.tablewidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.tablewidget.verticalHeader().setVisible(False) self.tablewidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tablewidget.setShowGrid(False) self.tablewidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tablewidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive) self.tablewidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.Interactive) self.tablewidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.Interactive) self.tablewidget.horizontalHeader().setSectionResizeMode(3, QHeaderView.Interactive) self.tablewidget.horizontalHeader().setSectionResizeMode(4, QHeaderView.Interactive) self.tablewidget.setFocusPolicy(Qt.NoFocus) # 去除选中后的虚线框 self.tablewidget.itemDoubleClicked.connect(self.tablewidget_double_clicked) self.tablewidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tablewidget.customContextMenuRequested['QPoint'].connect(self.tablewidget_right_menu) table_header = self.tablewidget.horizontalHeader() table_header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) header_font = table_header.font() header_font.setBold(True) table_header.setFont(header_font) table_header.setStyleSheet(''' QHeaderView::section{ padding-left: 30px; height:40px; font-size:18px; background-color: #D1D1D1; } ''') self.tablewidget.setRowCount(rows) self.tablewidget.setHorizontalHeaderLabels(['文件名', '最后修改日期', '类型', '大小', '位置']) for row_num in range(rows): # 行号 if os.path.isdir(os.path.join(file_path,file_list[row_num])): file_type = '文件夹' statinfo = os.stat(os.path.join(file_path, file_list[row_num])) file_pos = file_path file_size = self.approximateSize(self.getdirsize(os.path.join(file_path,file_list[row_num]))) last_update_time = self.time_format(statinfo.st_mtime) # 最后修改时间 file_info = [file_list[row_num],last_update_time,file_type,file_size,file_pos] else: if file_list[row_num].endswith('.jpg'): file_type = 'JPG图片文件( *.jpg )' file_image = '../images/jpg_status.png' elif file_list[row_num].endswith('.html'): file_type = 'HTML页面文件( *.html )' file_image = '../images/html_status.png' elif file_list[row_num].endswith('.xlsx'): file_type = 'XLSX表格文件( *.xlsx )' file_image = '../images/excel_status.png' elif file_list[row_num].endswith('.png'): file_type = 'PNG图片文件( *.png )' file_image = '../images/png_status.png' elif file_list[row_num].endswith('.txt'): file_type = 'txt图片文件( *.txt )' file_image = '../images/txt_status.png' else: file_type = 'Other其他文件类型( *.%s)' % (os.path.splitext(file_list[row_num])[1]) file_image = '../images/file_status.png' statinfo = os.stat(os.path.join(file_path, file_list[row_num])) last_update_time = self.time_format(statinfo.st_mtime) # 最后修改时间 file_size = self.approximateSize(statinfo.st_size) file_info = [file_list[row_num], last_update_time, file_type,file_size,file_path] # print(file_info) for i in range(5): item = QTableWidgetItem(file_info[i]) self.tablewidget.setItem(row_num,i,item) self.tablewidget.setRowHeight(row_num,50) self.tablewidget.setStyleSheet(''' QTableWidget { background:transparent; } QTableWidget::item { padding: 10px; border: 0px solid red; } QTableWidget::item:selected { color: black; background-color: rgb(102,204,204); } ''') return self.tablewidget def tablewidget_right_menu(self,pos,file_path=None): item_row = self.tablewidget.currentRow() file_name = self.tablewidget.item(item_row,0).text() file_path = self.tablewidget.item(item_row,4).text() if not file_path: file_path = os.path.join(self.paths[-1],file_name) file_path = file_path.replace('\\','/') # print(file_path) menu = QMenu(self.tablewidget) delete = menu.addAction('删除') copy = menu.addAction('复制') paste = menu.addAction('粘贴') openLocalFile = menu.addAction('浏览本地文件') file_roperty = menu.addAction("属性") action = menu.exec_(self.tablewidget.mapToGlobal(pos)) if action == delete: reply = QMessageBox.warning(self.mainwindow, '删除确认', '确认删除吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if os.path.isdir(file_path) and reply == 16384: # print('delete dir') shutil.rmtree(file_path) self.statusBar.showMessage(f'删除 {file_path} 成功!') self.updateFileTree() elif not os.path.isdir(file_path) and reply == 16384: # print('delete file') os.remove(file_path) self.statusBar.showMessage("删除 -> %s 成功!" % file_path) # print(self.paths) self.updateFileTree() self.statusBar.showMessage(f'删除 {file_path} 成功!') elif action == copy: try: data = QMimeData() url = QUrl.fromLocalFile(file_path) clipboard = QApplication.clipboard() data.setUrls([url]) clipboard.setMimeData(data) self.statusBar.showMessage("已复制 -> %s 到剪切板" % file_path) except Exception as e: QMessageBox.about(self.mainwindow, '错误', '文件不存在!') self.statusBar.showMessage("复制 -> %s 出错,文件不存在!" % file_path) elif action == paste: data = QApplication.clipboard().mimeData() source_file_url = data.urls()[0].url() self.paste_thread = FilePasteThread(source_file_url[8:], file_path) self.paste_thread.sinOut.connect(self.filePasteComplete) self.paste_thread.start() elif action == openLocalFile: try: local_path = file_path.replace('/', '\\') os.system("explorer.exe %s" % os.path.dirname(local_path)) except Exception as e: QMessageBox.warning(self.mainwindow, '错误', '打开文件不存在!') elif action == file_roperty: # print('查看文件属性') if os.path.isdir(file_path): file_type = '文件夹' file_image = '../images/folder_status.png' _dir = True else: _dir = False if file_path.endswith('.jpg'): file_type = 'JPG图片文件( *.jpg )' file_image = '../images/jpg_status.png' elif file_path.endswith('.html'): file_type = 'HTML页面文件( *.html )' file_image = '../images/html_status.png' elif file_path.endswith('.xlsx'): file_type = 'XLSX表格文件( *.xlsx )' file_image = '../images/excel_status.png' elif file_path.endswith('.png'): file_type = 'PNG表格文件( *.png )' file_image = '../images/png_status.png' else: file_type = 'Other其他文件类型( *.%s)' % (os.path.splitext(file_path)[1]) file_image = '../images/file_status.png' if _dir: '''文件夹大小去要遍历每个子文件夹与文件累加''' file_size = self.getdirsize(file_path) # print(file_path) statinfo = os.stat(file_path) else: statinfo = os.stat(file_path) file_size = statinfo.st_size file_atime = self.time_format(statinfo.st_atime) # 文件最后访问时间 file_ctime = self.time_format(statinfo.st_ctime) # 文件创建时间 file_mtime = self.time_format(statinfo.st_mtime) # 文件最后修改时间 self.file_status_window = FileStatusWindow() self.file_status_window.filename = file_path.replace('\\', '/').split('/')[-1] self.status_main_window = QMainWindow(MainWindow) self.file_status_window.setupUi(self.status_main_window) self.file_status_window.lineEdit.setText(self.file_status_window.filename) self.file_status_window.label_3.setText(file_type) self.file_status_window.label_5.setText(file_path.replace('/', '\\')) self.file_status_window.label_9.setText(file_ctime) self.file_status_window.label_11.setText(file_mtime) self.file_status_window.label_13.setText(file_atime) self.file_status_window.label_7.setText(self.approximateSize(file_size)) self.file_status_window.pushButton.clicked.connect(self.fileStatusUse) # 应用按钮click出发函数 self.file_status_window.pushButton_2.clicked.connect(self.fileStatusConfirm) self.file_status_window.pushButton_3.clicked.connect(self.fileStatusCancel) pix = QPixmap(file_image) self.file_status_window.label.setPixmap(pix) self.status_main_window.show() def tablewidget_double_clicked(self): '''文件管理列表双击事件''' row = self.tablewidget.currentRow() # 拿到当前行 file_path = self.tablewidget.item(row,4).text() # 文件上级路径 file_name = self.tablewidget.item(row,0).text() # 文件名 file_full_name = os.path.join(file_path,file_name) # 文件绝对路径 if os.path.isdir(file_full_name): '''文件夹执行双击打操作''' self.paths.append(file_path) # 打开文件夹 记录上级 self.tablewidget.clear() self.get_file_list(file_full_name,self.tablewidget) else: '''文件打开操作''' if file_full_name.endswith('jpg') or file_full_name.endswith('.png') or file_full_name.endswith( 'html') or file_full_name.endswith('pdf'): file_path = file_full_name.replace('\\', '/') if file_path.endswith('.html'): self.browser.stop() f = open(file_path,'r',encoding='utf-8') html = f.read() self.browser.setHtml(html) self.browser.show() else: self.browser.stop() self.browser.load(QUrl('file:///' + file_path)) self.browser.show() self.zoom_in_button.clicked.connect(self.zoom_in_func) # 放大与缩小按钮出发事件设置 self.zoom_out_button.clicked.connect(self.zoom_out_func) self.sp.setValue(self.browser.zoomFactor()) filename = file_path.split('/')[-1] self.tabWidget.setCurrentIndex(1) self.statusBar.showMessage(f'预览文件 -> {filename} ') # self.browser.loadFinished.connect(self.browser_finished) else: reply = QMessageBox.information(self.mainwindow, "调用系统软件", "软件不支持此类型文件打开!\n是否调用系统程序打开此文件?", QMessageBox.Yes | QMessageBox.No) if reply == 16384: os.startfile(file_path) def file_back(self): # print(self.paths) self.tablewidget.clear() if len(self.paths) == 1: file_path = self.paths[-1] self.get_file_list(file_path,self.tablewidget) else: self.paths.pop() self.get_file_list(self.paths[-1],self.tablewidget)