class XSplitButton(QWidget): """ ~~>[img:widgets/xsplitbutton.png] The XSplitButton class provides a simple class for creating a multi-checkable tool button based on QActions and QActionGroups. === Example Usage === |>>> from projexui.widgets.xsplitbutton import XSplitButton |>>> import projexui | |>>> # create the widget |>>> widget = projexui.testWidget(XSplitButton) | |>>> # add some actions (can be text or a QAction) |>>> widget.addAction('Day') |>>> widget.addAction('Month') |>>> widget.addAction('Year') | |>>> # create connections |>>> def printAction(act): print act.text() |>>> widget.actionGroup().triggered.connect(printAction) """ __designer_icon__ = projexui.resources.find('img/ui/multicheckbox.png') clicked = Signal() currentActionChanged = Signal(object) hovered = Signal(object) triggered = Signal(object) def __init__( self, parent = None ): super(XSplitButton, self).__init__( parent ) # define custom properties self._actionGroup = QActionGroup(self) self._padding = 5 self._cornerRadius = 10 #self._currentAction = None self._checkable = True # set default properties layout = QBoxLayout(QBoxLayout.LeftToRight) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.setLayout(layout) self.clear() # create connections self._actionGroup.hovered.connect(self.emitHovered) self._actionGroup.triggered.connect(self.emitTriggered) def actions(self): """ Returns a list of the actions linked with this widget. :return [<QAction>, ..] """ return self._actionGroup.actions() def actionTexts(self): """ Returns a list of the action texts for this widget. :return [<str>, ..] """ return map(lambda x: x.text(), self._actionGroup.actions()) def actionGroup( self ): """ Returns the action group linked with this widget. :return <QActionGroup> """ return self._actionGroup def addAction(self, action, checked=None, autoBuild=True): """ Adds the inputed action to this widget's action group. This will auto-\ create a new group if no group is already defined. :param action | <QAction> || <str> :return <QAction> """ # clear the holder actions = self._actionGroup.actions() if actions and actions[0].objectName() == 'place_holder': self._actionGroup.removeAction(actions[0]) actions[0].deleteLater() # create an action from the name if not isinstance(action, QAction): action_name = str(action) action = QAction(action_name, self) action.setObjectName(action_name) action.setCheckable(self.isCheckable()) # auto-check the first option if checked or (not self._actionGroup.actions() and checked is None): action.setChecked(True) self._actionGroup.addAction(action) if autoBuild: self.rebuild() return action def clear(self, autoBuild=True): """ Clears the actions for this widget. """ for action in self._actionGroup.actions(): self._actionGroup.removeAction(action) action = QAction('', self) action.setObjectName('place_holder') # self._currentAction = None self._actionGroup.addAction(action) if autoBuild: self.rebuild() def cornerRadius( self ): """ Returns the corner radius for this widget. :return <int> """ return self._cornerRadius def count(self): """ Returns the number of actions associated with this button. :return <int> """ actions = self._actionGroup.actions() if len(actions) == 1 and actions[0].objectName() == 'place_holder': return 0 return len(actions) def currentAction( self ): """ Returns the action that is currently checked in the system. :return <QAction> || None """ return self._actionGroup.checkedAction() def direction( self ): """ Returns the direction for this widget. :return <QBoxLayout::Direction> """ return self.layout().direction() def emitClicked(self): """ Emits the clicked signal whenever any of the actions are clicked. """ if not self.signalsBlocked(): self.clicked.emit() def emitHovered(self, action): """ Emits the hovered action for this widget. :param action | <QAction> """ if not self.signalsBlocked(): self.hovered.emit(action) def emitTriggered(self, action): """ Emits the triggered action for this widget. :param action | <QAction> """ # if action != self._currentAction: # self._currentAction = action # self.currentActionChanged.emit(action) # self._currentAction = action if not self.signalsBlocked(): self.triggered.emit(action) def findAction( self, text ): """ Looks up the action based on the inputed text. :return <QAction> || None """ for action in self.actionGroup().actions(): if ( text in (action.objectName(), action.text()) ): return action return None def isCheckable(self): """ Returns whether or not the actions within this button should be checkable. :return <bool> """ return self._checkable def padding( self ): """ Returns the button padding amount for this widget. :return <int> """ return self._padding def rebuild( self ): """ Rebuilds the user interface buttons for this widget. """ self.setUpdatesEnabled(False) # sync up the toolbuttons with our actions actions = self._actionGroup.actions() btns = self.findChildren(QToolButton) horiz = self.direction() in (QBoxLayout.LeftToRight, QBoxLayout.RightToLeft) # remove unnecessary buttons if len(actions) < len(btns): rem_btns = btns[len(actions)-1:] btns = btns[:len(actions)] for btn in rem_btns: btn.close() btn.setParent(None) btn.deleteLater() # create new buttons elif len(btns) < len(actions): for i in range(len(btns), len(actions)): btn = QToolButton(self) btn.setAutoFillBackground(True) btns.append(btn) self.layout().addWidget(btn) btn.clicked.connect(self.emitClicked) # determine coloring options palette = self.palette() checked = palette.color(palette.Highlight) checked_fg = palette.color(palette.HighlightedText) unchecked = palette.color(palette.Button) unchecked_fg = palette.color(palette.ButtonText) border = palette.color(palette.Mid) # define the stylesheet options options = {} options['top_left_radius'] = 0 options['top_right_radius'] = 0 options['bot_left_radius'] = 0 options['bot_right_radius'] = 0 options['border_color'] = border.name() options['checked_fg'] = checked_fg.name() options['checked_bg'] = checked.name() options['checked_bg_alt'] = checked.darker(120).name() options['unchecked_fg'] = unchecked_fg.name() options['unchecked_bg'] = unchecked.name() options['unchecked_bg_alt'] = unchecked.darker(120).name() options['padding_top'] = 1 options['padding_bottom'] = 1 options['padding_left'] = 1 options['padding_right'] = 1 if horiz: options['x1'] = 0 options['y1'] = 0 options['x2'] = 0 options['y2'] = 1 else: options['x1'] = 0 options['y1'] = 0 options['x2'] = 1 options['y2'] = 1 # sync up the actions and buttons count = len(actions) palette = self.palette() font = self.font() for i, action in enumerate(actions): btn = btns[i] # assign the action for this button if btn.defaultAction() != action: # clear out any existing actions for act in btn.actions(): btn.removeAction(act) # assign the given action btn.setDefaultAction(action) options['top_left_radius'] = 1 options['bot_left_radius'] = 1 options['top_right_radius'] = 1 options['bot_right_radius'] = 1 if horiz: options['padding_left'] = self._padding options['padding_right'] = self._padding else: options['padding_top'] = self._padding options['padding_bottom'] = self._padding if not i: if horiz: options['top_left_radius'] = self.cornerRadius() options['bot_left_radius'] = self.cornerRadius() options['padding_left'] += self.cornerRadius() / 3.0 else: options['top_left_radius'] = self.cornerRadius() options['top_right_radius'] = self.cornerRadius() options['padding_top'] += self.cornerRadius() / 3.0 if i == count - 1: if horiz: options['top_right_radius'] = self.cornerRadius() options['bot_right_radius'] = self.cornerRadius() options['padding_right'] += self.cornerRadius() / 3.0 else: options['bot_left_radius'] = self.cornerRadius() options['bot_right_radius'] = self.cornerRadius() options['padding_bottom'] += self.cornerRadius() / 3.0 btn.setFont(font) btn.setPalette(palette) btn.setStyleSheet(TOOLBUTTON_STYLE % options) if horiz: btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) else: btn.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.setUpdatesEnabled(True) def setActions(self, actions): """ Sets the actions for this widget to th inputed list of actions. :param [<QAction>, ..] """ self.clear(autoBuild=False) for action in actions: self.addAction(action, autoBuild=False) self.rebuild() def setActionTexts(self, names): """ Convenience method for auto-generating actions based on text names, sets the list of actions for this widget to the inputed list of names. :param names | [<str>, ..] """ self.setActions(names) def setActionGroup( self, actionGroup ): """ Sets the action group for this widget to the inputed action group. :param actionGroup | <QActionGroup> """ self._actionGroup = actionGroup self.rebuild() def setCheckable(self, state): """ Sets whether or not the actions within this button should be checkable. :param state | <bool> """ self._checkable = state for act in self._actionGroup.actions(): act.setCheckable(state) def setCornerRadius( self, radius ): """ Sets the corner radius value for this widget to the inputed radius. :param radius | <int> """ self._cornerRadius = radius def setCurrentAction(self, action): """ Sets the current action for this button to the inputed action. :param action | <QAction> || <str> """ self._actionGroup.blockSignals(True) for act in self._actionGroup.actions(): act.setChecked(act == action or act.text() == action) self._actionGroup.blockSignals(False) def setDirection( self, direction ): """ Sets the direction that this group widget will face. :param direction | <QBoxLayout::Direction> """ self.layout().setDirection(direction) self.rebuild() def setFont(self, font): """ Sets the font for this widget and propogates down to the buttons. :param font | <QFont> """ super(XSplitButton, self).setFont(font) self.rebuild() def setPadding( self, padding ): """ Sets the padding amount for this widget's button set. :param padding | <int> """ self._padding = padding self.rebuild() def setPalette(self, palette): """ Rebuilds the buttons for this widget since they use specific palette options. :param palette | <QPalette> """ super(XSplitButton, self).setPalette(palette) self.rebuild() def sizeHint(self): """ Returns the base size hint for this widget. :return <QSize> """ return QSize(35, 22) x_actionTexts = Property(QStringList, actionTexts, setActionTexts) x_checkable = Property(bool, isCheckable, setCheckable)
class XViewPanelMenu(XMenu): _instance = None def __init__(self, viewWidget, interfaceMode = False): # initialize the super class super(XViewPanelMenu, self).__init__( viewWidget ) # define custom properties self._viewWidget = viewWidget self._currentPanel = None self._interfaceMenu = None self._groupingMenu = None self._panelGroup = QActionGroup(self) self._groupingGroup = QActionGroup(self) # initialize the menu if not interfaceMode: self.setTitle('Panel Options') self.addSection('Panels') act = self.addAction('Split Panel Left/Right') act.setIcon(QIcon(resources.find('img/view/split_horizontal.png'))) act.triggered.connect(self.splitHorizontal) act = self.addAction('Split Panel Top/Bottom') act.setIcon(QIcon(resources.find('img/view/split_vertical.png'))) act.triggered.connect(self.splitVertical) self.addSeparator() act = self.addAction('Add Panel') act.setIcon(QIcon(resources.find('img/view/add.png'))) act.triggered.connect(self.addPanel) # create pane options menu self._interfaceMenu = XViewPanelMenu(viewWidget, True) self.addMenu(self._interfaceMenu) menu = self.addMenu('Switch Panels') menu.setIcon(QIcon(resources.find('img/view/switch.png'))) act = menu.addAction('Move Up') act.setIcon(QIcon(resources.find('img/view/up.png'))) act = menu.addAction('Move Down') act.setIcon(QIcon(resources.find('img/view/down.png'))) menu.addSeparator() act = menu.addAction('Move Left') act.setIcon(QIcon(resources.find('img/view/left.png'))) act = menu.addAction('Move Right') act.setIcon(QIcon(resources.find('img/view/right.png'))) menu.triggered.connect(self.switchPanels) set_tab_menu = self.addMenu('Switch View') for viewType in viewWidget.viewTypes(): act = set_tab_menu.addAction(viewType.viewName()) act.setIcon(QIcon(viewType.viewIcon())) act.setCheckable(True) self._panelGroup.addAction(act) set_tab_menu.triggered.connect( self.swapTabType ) self.addSeparator() act = self.addAction('Close Panel (Closes All Tabs)') act.setIcon(QIcon(resources.find('img/view/close.png'))) act.triggered.connect( self.closePanel ) act = self.addAction('Close All Panels (Clears Dashboard)') act.setIcon(QIcon(resources.find('img/view/reset.png'))) act.triggered.connect( self.reset ) self.addSection('Tabs') act = self.addAction('Rename Tab') act.setIcon(QIcon(resources.find('img/edit.png'))) act.triggered.connect( self.renamePanel ) act = self.addAction('Detach Tab') act.setIcon(QIcon(resources.find('img/view/detach.png'))) act.triggered.connect( self.detachPanel ) act = self.addAction('Detach Tab (as a Copy)') act.setIcon(QIcon(resources.find('img/view/detach_copy.png'))) act.triggered.connect( self.detachPanelCopy ) act = self.addAction('Close Tab') act.setIcon(QIcon(resources.find('img/view/tab_remove.png'))) act.triggered.connect( self.closeView ) self.addSection('Views') # create view grouping options self._groupingMenu = self.addMenu('Set Group') icon = QIcon(resources.find('img/view/group.png')) self._groupingMenu.setIcon(icon) act = self._groupingMenu.addAction('No Grouping') act.setData(qt.wrapVariant(0)) act.setCheckable(True) self._groupingMenu.addSeparator() self._groupingGroup.addAction(act) for i in range(1, 6): act = self._groupingMenu.addAction('Group %i' % i) act.setData(qt.wrapVariant(i)) act.setCheckable(True) self._groupingGroup.addAction(act) self._groupingMenu.triggered.connect(self.assignGroup) else: self.setTitle( 'Add View' ) for viewType in viewWidget.viewTypes(): act = self.addAction(viewType.viewName()) act.setIcon(QIcon(viewType.viewIcon())) self.triggered.connect( self.addView ) self.addSeparator() def addPanel( self ): panel = self.currentPanel() if panel is not None: return panel.addPanel() return None def addView( self, action ): panel = self.currentPanel() if ( panel is None ): return False viewType = self._viewWidget.viewType(action.text()) return panel.addView(viewType) def assignGroup( self, action ): """ Assigns the group for the given action to the current view. :param action | <QAction> """ grp = qt.unwrapVariant(action.data()) view = self._currentPanel.currentView() view.setViewingGroup(grp) def closePanel( self ): """ Closes the current panel within the view widget. """ panel = self.currentPanel() if panel is not None: return panel.closePanel() return False def closeView( self ): """ Closes the current view within the panel. """ panel = self.currentPanel() if ( panel is not None ): return panel.closeView() return False def currentPanel( self ): """ Returns the current panel widget. :return <XViewPanel> """ return self._currentPanel def detachPanel( self ): """ Detaches the current panel as a floating window. """ #from projexui.widgets.xviewwidget import XViewDialog dlg = XViewDialog(self._viewWidget, self._viewWidget.viewTypes()) size = self._currentPanel.size() dlg.viewWidget().currentPanel().snagViewFromPanel(self._currentPanel) dlg.resize(size) dlg.show() def detachPanelCopy( self ): """ Detaches the current panel as a floating window. """ #from projexui.widgets.xviewwidget import XViewDialog dlg = XViewDialog(self._viewWidget, self._viewWidget.viewTypes()) size = self._currentPanel.size() view = self._currentPanel.currentView() # duplicate the current view if ( view ): new_view = view.duplicate(dlg.viewWidget().currentPanel()) view_widget = dlg.viewWidget() view_panel = view_widget.currentPanel() view_panel.addTab(new_view, new_view.windowTitle()) dlg.resize(size) dlg.show() def gotoNext( self ): """ Goes to the next panel tab. """ index = self._currentPanel.currentIndex() + 1 if ( self._currentPanel.count() == index ): index = 0 self._currentPanel.setCurrentIndex(index) def gotoPrevious( self ): """ Goes to the previous panel tab. """ index = self._currentPanel.currentIndex() - 1 if ( index < 0 ): index = self._currentPanel.count() - 1 self._currentPanel.setCurrentIndex(index) def newPanelTab( self ): """ Creates a new panel with a copy of the current widget. """ view = self._currentPanel.currentView() # duplicate the current view if ( view ): new_view = view.duplicate(self._currentPanel) self._currentPanel.addTab(new_view, new_view.windowTitle()) def renamePanel( self ): """ Prompts the user for a custom name for the current panel tab. """ index = self._currentPanel.currentIndex() title = self._currentPanel.tabText(index) new_title, accepted = QInputDialog.getText( self, 'Rename Tab', 'Name:', QLineEdit.Normal, title ) if ( accepted ): widget = self._currentPanel.currentView() widget.setWindowTitle(new_title) self._currentPanel.setTabText(index, new_title) def reset( self ): """ Clears the current views from the system """ self._viewWidget.reset() def setCurrentPanel( self, panel ): self._currentPanel = panel # update the current tab based on what view type it is viewType = '' grp = -1 if ( panel is not None and panel.currentView() ): viewType = panel.currentView().viewName() grp = panel.currentView().viewingGroup() self._panelGroup.blockSignals(True) for act in self._panelGroup.actions(): act.setChecked(viewType == act.text()) self._panelGroup.blockSignals(False) self._groupingGroup.blockSignals(True) for act in self._groupingGroup.actions(): act.setChecked(grp == qt.unwrapVariant(act.data())) self._groupingGroup.blockSignals(False) if ( self._groupingMenu ): self._groupingMenu.setEnabled(grp != -1) if ( self._interfaceMenu ): self._interfaceMenu.setCurrentPanel(panel) def splitVertical( self ): panel = self.currentPanel() if ( panel is not None ): return panel.splitVertical() return None def splitHorizontal( self ): panel = self.currentPanel() if ( panel is not None ): return panel.splitHorizontal() return None def switchPanels(self, action): direction = action.text() if direction in ('Move Up', 'Move Down'): orientation = Qt.Vertical else: orientation = Qt.Horizontal widget = self.currentPanel() if not widget: return # look up the splitter for the widget splitter = widget.parent() while widget and isinstance(splitter, QSplitter): if splitter.orientation() == orientation: break widget = splitter splitter = splitter.parent() if not isinstance(splitter, QSplitter): return # determine the new location for the panel index = splitter.indexOf(widget) if direction in ('Move Down', 'Move Right'): new_index = index widget = splitter.widget(index + 1) else: new_index = index - 1 if widget and 0 <= new_index and new_index < splitter.count(): splitter.insertWidget(new_index, widget) def swapTabType( self, action ): """ Swaps the current tab view for the inputed action's type. :param action | <QAction> """ # for a new tab, use the add tab slot if not self._currentPanel.count(): self.addView(action) return # make sure we're not trying to switch to the same type viewType = self._viewWidget.viewType(action.text()) view = self._currentPanel.currentView() if ( type(view) == viewType ): return # create a new view and close the old one self._currentPanel.setUpdatesEnabled(False) # create the new view new_view = viewType.createInstance(self._currentPanel) index = self._currentPanel.currentIndex() # cleanup the view view.destroyInstance(view) # add the new view self._currentPanel.insertTab(index - 1, new_view, new_view.windowTitle()) self._currentPanel.setUpdatesEnabled(True)