class ToolBox(QFrame): """ A tool box widget. """ # Emitted when a tab is toggled. tabToogled = Signal(int, bool) def setExclusive(self, exclusive): """ Set exclusive tabs (only one tab can be open at a time). """ if self.__exclusive != exclusive: self.__exclusive = exclusive self.__tabActionGroup.setExclusive(exclusive) checked = self.__tabActionGroup.checkedAction() if checked is None: # The action group can be out of sync with the actions state # when switching between exclusive states. actions_checked = [ page.action for page in self.__pages if page.action.isChecked() ] if actions_checked: checked = actions_checked[0] # Trigger/toggle remaining open pages if exclusive and checked is not None: for page in self.__pages: if checked != page.action and page.action.isChecked(): page.action.trigger() def exclusive(self): """ Are the tabs in the toolbox exclusive. """ return self.__exclusive exclusive_ = Property(bool, fget=exclusive, fset=setExclusive, designable=True, doc="Exclusive tabs") def __init__(self, parent=None, **kwargs): QFrame.__init__(self, parent, **kwargs) self.__pages = [] self.__tabButtonHeight = -1 self.__tabIconSize = QSize() self.__exclusive = False self.__setupUi() def __setupUi(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) # Scroll area for the contents. self.__scrollArea = \ _ToolBoxScrollArea(self, objectName="toolbox-scroll-area") self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.__scrollArea.setFrameStyle(QScrollArea.NoFrame) self.__scrollArea.setWidgetResizable(True) # A widget with all of the contents. # The tabs/contents are placed in the layout inside this widget self.__contents = QWidget(self.__scrollArea, objectName="toolbox-contents") self.__contentsLayout = _ToolBoxLayout( sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize, spacing=0) self.__contentsLayout.setContentsMargins(0, 0, 0, 0) self.__contents.setLayout(self.__contentsLayout) self.__scrollArea.setWidget(self.__contents) layout.addWidget(self.__scrollArea) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self.__tabActionGroup = \ QActionGroup(self, objectName="toolbox-tab-action-group") self.__tabActionGroup.setExclusive(self.__exclusive) self.__actionMapper = QSignalMapper(self) self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled) def setTabButtonHeight(self, height): """ Set the tab button height. """ if self.__tabButtonHeight != height: self.__tabButtonHeight = height for page in self.__pages: page.button.setFixedHeight(height) def tabButtonHeight(self): """ Return the tab button height. """ return self.__tabButtonHeight def setTabIconSize(self, size): """ Set the tab button icon size. """ if self.__tabIconSize != size: self.__tabIconSize = size for page in self.__pages: page.button.setIconSize(size) def tabIconSize(self): """ Return the tab icon size. """ return self.__tabIconSize def tabButton(self, index): """ Return the tab button at `index` """ return self.__pages[index].button def tabAction(self, index): """ Return open/close action for the tab at `index`. """ return self.__pages[index].action def addItem(self, widget, text, icon=None, toolTip=None): """ Append the `widget` in a new tab and return its index. Parameters ---------- widget : :class:`QWidget` A widget to be inserted. The toolbox takes ownership of the widget. text : str Name/title of the new tab. icon : :class:`QIcon`, optional An icon for the tab button. toolTip : str, optional Tool tip for the tab button. """ return self.insertItem(self.count(), widget, text, icon, toolTip) def insertItem(self, index, widget, text, icon=None, toolTip=None): """ Insert the `widget` in a new tab at position `index`. See also -------- ToolBox.addItem """ button = self.createTabButton(widget, text, icon, toolTip) self.__contentsLayout.insertWidget(index * 2, button) self.__contentsLayout.insertWidget(index * 2 + 1, widget) widget.hide() page = _ToolBoxPage(index, widget, button.defaultAction(), button) self.__pages.insert(index, page) for i in range(index + 1, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) self.__updatePositions() # Show (open) the first tab. if self.count() == 1 and index == 0: page.action.trigger() self.__updateSelected() self.updateGeometry() return index def removeItem(self, index): """ Remove the widget at `index`. .. note:: The widget hidden but is is not deleted. """ self.__contentsLayout.takeAt(2 * index + 1) self.__contentsLayout.takeAt(2 * index) page = self.__pages.pop(index) # Update the page indexes for i in range(index, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) page.button.deleteLater() # Hide the widget and reparent to self # This follows QToolBox.removeItem page.widget.hide() page.widget.setParent(self) self.__updatePositions() self.__updateSelected() self.updateGeometry() def count(self): """ Return the number of widgets inserted in the toolbox. """ return len(self.__pages) def widget(self, index): """ Return the widget at `index`. """ return self.__pages[index].widget def createTabButton(self, widget, text, icon=None, toolTip=None): """ Create the tab button for `widget`. """ action = QAction(text, self) action.setCheckable(True) if icon: action.setIcon(icon) if toolTip: action.setToolTip(toolTip) self.__tabActionGroup.addAction(action) self.__actionMapper.setMapping(action, action) action.toggled.connect(self.__actionMapper.map) button = ToolBoxTabButton(self, objectName="toolbox-tab-button") button.setDefaultAction(action) button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) if self.__tabIconSize.isValid(): button.setIconSize(self.__tabIconSize) if self.__tabButtonHeight > 0: button.setFixedHeight(self.__tabButtonHeight) return button def ensureWidgetVisible(self, child, xmargin=50, ymargin=50): """ Scroll the contents so child widget instance is visible inside the viewport. """ self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin) def sizeHint(self): hint = self.__contentsLayout.sizeHint() if self.count(): # Compute max width of hidden widgets also. scroll = self.__scrollArea scroll_w = scroll.verticalScrollBar().sizeHint().width() frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2 max_w = max([p.widget.sizeHint().width() for p in self.__pages]) hint = QSize( max(max_w, hint.width()) + scroll_w + frame_w, hint.height()) return QSize(200, 200).expandedTo(hint) def __onTabActionToogled(self, action): page = find(self.__pages, action, key=attrgetter("action")) on = action.isChecked() page.widget.setVisible(on) index = page.index if index > 0: # Update the `previous` tab buttons style hints previous = self.__pages[index - 1].button flag = QStyleOptionToolBox.NextIsSelected if on: previous.selected |= flag else: previous.selected &= ~flag previous.update() if index < self.count() - 1: next = self.__pages[index + 1].button flag = QStyleOptionToolBox.PreviousIsSelected if on: next.selected |= flag else: next.selected &= ~flag next.update() self.tabToogled.emit(index, on) self.__contentsLayout.invalidate() def __updateSelected(self): """Update the tab buttons selected style flags. """ if self.count() == 0: return opt = QStyleOptionToolBox def update(button, next_sel, prev_sel): if next_sel: button.selected |= opt.NextIsSelected else: button.selected &= ~opt.NextIsSelected if prev_sel: button.selected |= opt.PreviousIsSelected else: button.selected &= ~opt.PreviousIsSelected button.update() if self.count() == 1: update(self.__pages[0].button, False, False) elif self.count() >= 2: pages = self.__pages for i in range(1, self.count() - 1): update(pages[i].button, pages[i + 1].action.isChecked(), pages[i - 1].action.isChecked()) def __updatePositions(self): """Update the tab buttons position style flags. """ if self.count() == 0: return elif self.count() == 1: self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab else: self.__pages[0].button.position = QStyleOptionToolBox.Beginning self.__pages[-1].button.position = QStyleOptionToolBox.End for p in self.__pages[1:-1]: p.button.position = QStyleOptionToolBox.Middle for p in self.__pages: p.button.update()
class ImageMarkupDialog(QDialog, Ui_ImageMarkupDialog): """ Class implementing a dialog to enter data for an image markup. """ HtmlMode = 0 MarkDownMode = 1 RestMode = 2 def __init__(self, mode, parent=None): """ Constructor @param mode mode of the dialog @type int @param parent reference to the parent widget @type QWidget """ super(ImageMarkupDialog, self).__init__(parent) self.setupUi(self) if mode == ImageMarkupDialog.MarkDownMode: self.sizeCheckBox.setEnabled(False) self.aspectRatioCheckBox.setEnabled(False) self.widthSpinBox.setEnabled(False) self.heightSpinBox.setEnabled(False) elif mode == ImageMarkupDialog.RestMode: self.titleEdit.setEnabled(False) self.__mode = mode self.__originalImageSize = QSize() filters = { 'bmp': self.tr("Windows Bitmap File (*.bmp)"), 'cur': self.tr("Windows Cursor File (*.cur)"), 'dds': self.tr("DirectDraw-Surface File (*.dds)"), 'gif': self.tr("Graphic Interchange Format File (*.gif)"), 'icns': self.tr("Apple Icon File (*.icns)"), 'ico': self.tr("Windows Icon File (*.ico)"), 'jp2': self.tr("JPEG2000 File (*.jp2)"), 'jpg': self.tr("JPEG File (*.jpg)"), 'jpeg': self.tr("JPEG File (*.jpeg)"), 'mng': self.tr("Multiple-Image Network Graphics File (*.mng)"), 'pbm': self.tr("Portable Bitmap File (*.pbm)"), 'pcx': self.tr("Paintbrush Bitmap File (*.pcx)"), 'pgm': self.tr("Portable Graymap File (*.pgm)"), 'png': self.tr("Portable Network Graphics File (*.png)"), 'ppm': self.tr("Portable Pixmap File (*.ppm)"), 'sgi': self.tr("Silicon Graphics Image File (*.sgi)"), 'svg': self.tr("Scalable Vector Graphics File (*.svg)"), 'svgz': self.tr("Compressed Scalable Vector Graphics File" " (*.svgz)"), 'tga': self.tr("Targa Graphic File (*.tga)"), 'tif': self.tr("TIFF File (*.tif)"), 'tiff': self.tr("TIFF File (*.tiff)"), 'wbmp': self.tr("WAP Bitmap File (*.wbmp)"), 'webp': self.tr("WebP Image File (*.webp)"), 'xbm': self.tr("X11 Bitmap File (*.xbm)"), 'xpm': self.tr("X11 Pixmap File (*.xpm)"), } inputFormats = [] readFormats = QImageReader.supportedImageFormats() for readFormat in readFormats: try: inputFormats.append(filters[bytes(readFormat).decode()]) except KeyError: pass inputFormats.sort() inputFormats.append(self.tr("All Files (*)")) if filters["png"] in inputFormats: inputFormats.remove(filters["png"]) inputFormats.insert(0, filters["png"]) self.imagePicker.setFilters(';;'.join(inputFormats)) self.imagePicker.setMode(E5PathPickerModes.OpenFileMode) self.sizeCheckBox.setChecked(True) self.aspectRatioCheckBox.setChecked(True) msh = self.minimumSizeHint() self.resize(max(self.width(), msh.width()), msh.height()) self.__updateOkButton() def __updateOkButton(self): """ Private slot to set the state of the OK button. """ enable = bool(self.imagePicker.text()) if self.__mode == ImageMarkupDialog.MarkDownMode: enable = enable and bool(self.altTextEdit.text()) self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) @pyqtSlot(str) def on_imagePicker_textChanged(self, address): """ Private slot handling changes of the image path. @param address image address (URL or local path) @type str """ if address and "://" not in address: image = QImage(address) # load the file to set the size spin boxes if image.isNull(): self.widthSpinBox.setValue(0) self.heightSpinBox.setValue(0) self.__originalImageSize = QSize() self.__aspectRatio = 1 else: self.widthSpinBox.setValue(image.width()) self.heightSpinBox.setValue(image.height()) self.__originalImageSize = image.size() self.__aspectRatio = ( float(self.__originalImageSize.height()) / self.__originalImageSize.width()) else: self.widthSpinBox.setValue(0) self.heightSpinBox.setValue(0) self.__originalImageSize = QSize() self.__aspectRatio = 1 self.__updateOkButton() @pyqtSlot(str) def on_altTextEdit_textChanged(self, txt): """ Private slot handling changes of the alternative text. @param txt alternative text @type str """ self.__updateOkButton() @pyqtSlot(bool) def on_sizeCheckBox_toggled(self, checked): """ Private slot to reset the width and height spin boxes. @param checked flag indicating the state of the check box @type bool """ if checked: self.widthSpinBox.setValue(self.__originalImageSize.width()) self.heightSpinBox.setValue(self.__originalImageSize.height()) @pyqtSlot(bool) def on_aspectRatioCheckBox_toggled(self, checked): """ Private slot to adjust the height to match the original aspect ratio. @param checked flag indicating the state of the check box @type bool """ if checked and self.__originalImageSize.isValid(): height = self.widthSpinBox.value() * self.__aspectRatio self.heightSpinBox.setValue(height) @pyqtSlot(int) def on_widthSpinBox_valueChanged(self, width): """ Private slot to adjust the height spin box. @param width width for the image @type int """ if (self.aspectRatioCheckBox.isChecked() and self.widthSpinBox.hasFocus()): height = width * self.__aspectRatio self.heightSpinBox.setValue(height) @pyqtSlot(int) def on_heightSpinBox_valueChanged(self, height): """ Private slot to adjust the width spin box. @param height height for the image @type int """ if (self.aspectRatioCheckBox.isChecked() and self.heightSpinBox.hasFocus()): width = height / self.__aspectRatio self.widthSpinBox.setValue(width) def getData(self): """ Public method to get the entered data. @return tuple containing the image address, alternative text, title text, flag to keep the original size, width and height @rtype tuple of (str, str, str, bool, int, int) """ return ( self.imagePicker.text(), self.altTextEdit.text(), self.titleEdit.text(), self.sizeCheckBox.isChecked(), self.widthSpinBox.value(), self.heightSpinBox.value(), )