Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Canvas: Window Groups #3066

Merged
merged 6 commits into from
Jun 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Orange/canvas/application/canvasmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,11 +684,14 @@ def setup_menu(self):

# View menu
self.view_menu = QMenu(self.tr("&View"), self)
self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
self.view_menu)
self.toolbox_menu_group = \
QActionGroup(self, objectName="toolbox-menu-group")
# find and insert window group presets submenu
window_groups = self.scheme_widget.findChild(
QAction, "window-groups-action"
)
if isinstance(window_groups, QAction):
self.view_menu.addAction(window_groups)

self.view_menu.addSeparator()
self.view_menu.addAction(self.toggle_tool_dock_expand)
self.view_menu.addAction(self.show_log_action)
self.view_menu.addAction(self.show_report_action)
Expand Down
32 changes: 32 additions & 0 deletions Orange/canvas/document/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Undo/Redo Commands

"""
from typing import Callable

from AnyQt.QtWidgets import QUndoCommand

Expand Down Expand Up @@ -203,3 +204,34 @@ def redo(self):

def undo(self):
setattr(self.obj, self.attrname, self.oldvalue)


class SimpleUndoCommand(QUndoCommand):
"""
Simple undo/redo command specified by callable function pair.
Parameters
----------
redo: Callable[[], None]
A function expressing a redo action.
undo : Callable[[], None]
A function expressing a undo action.
text : str
The command's text (see `QUndoCommand.setText`)
parent : Optional[QUndoCommand]
"""

def __init__(self, redo, undo, text, parent=None):
# type: (Callable[[], None], Callable[[], None], ...) -> None
super().__init__(text, parent)
self._redo = redo
self._undo = undo

def undo(self):
# type: () -> None
"""Reimplemented."""
self._undo()

def redo(self):
# type: () -> None
"""Reimplemented."""
self._redo()
242 changes: 241 additions & 1 deletion Orange/canvas/document/schemeedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
from operator import attrgetter
from urllib.parse import urlencode

from typing import List

from AnyQt.QtWidgets import (
QWidget, QVBoxLayout, QInputDialog, QMenu, QAction, QActionGroup,
QUndoStack, QUndoCommand, QGraphicsItem, QGraphicsObject,
QGraphicsTextItem
QGraphicsTextItem, QFormLayout, QComboBox, QDialog, QDialogButtonBox,
QMessageBox
)
from AnyQt.QtGui import (
QKeySequence, QCursor, QFont, QPainter, QPixmap, QColor, QIcon,
Expand Down Expand Up @@ -352,6 +355,39 @@ def color_icon(color):
self.__selectAllAction,
self.__duplicateSelectedAction]

#: Top 'Window Groups' action
self.__windowGroupsAction = QAction(
self.tr("Window Groups"), self, objectName="window-groups-action",
toolTip="Manage preset widget groups"
)
#: Action group containing action for every window group
self.__windowGroupsActionGroup = QActionGroup(
self.__windowGroupsAction, objectName="window-groups-action-group",
)
self.__windowGroupsActionGroup.triggered.connect(
self.__activateWindowGroup
)
self.__saveWindowGroupAction = QAction(
self.tr("Save Window Group..."), self,
toolTip="Create and save a new window group."
)
self.__saveWindowGroupAction.triggered.connect(self.__saveWindowGroup)
self.__clearWindowGroupsAction = QAction(
self.tr("Delete All Groups"), self,
toolTip="Delete all saved widget presets"
)
self.__clearWindowGroupsAction.triggered.connect(
self.__clearWindowGroups
)

groups_menu = QMenu(self)
sep = groups_menu.addSeparator()
sep.setObjectName("groups-separator")
groups_menu.addAction(self.__saveWindowGroupAction)
groups_menu.addSeparator()
groups_menu.addAction(self.__clearWindowGroupsAction)
self.__windowGroupsAction.setMenu(groups_menu)

def __setupUi(self):
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
Expand Down Expand Up @@ -635,6 +671,28 @@ def setScheme(self, scheme):
if nodes:
self.ensureVisible(nodes[0])

group = self.__windowGroupsActionGroup
menu = self.__windowGroupsAction.menu()
actions = group.actions()
for a in actions:
group.removeAction(a)
menu.removeAction(a)
a.deleteLater()

if scheme:
presets = scheme.property("_presets") or []
sep = menu.findChild(QAction, "groups-separator")
assert isinstance(sep, QAction)
for name, state in presets:
a = QAction(name, menu)
a.setShortcut(
QKeySequence("Meta+P, Ctrl+{}"
.format(len(group.actions()) + 1))
)
a.setData(state)
group.addAction(a)
menu.insertAction(sep, a)

def ensureVisible(self, node):
"""
Scroll the contents of the viewport so that `node` is visible.
Expand Down Expand Up @@ -1634,6 +1692,188 @@ def __signalManagerStateChanged(self, state):
role = QPalette.Window
self.__view.viewport().setBackgroundRole(role)

def __saveWindowGroup(self):
# Run a 'Save Window Group' dialog
workflow = self.__scheme # type: widgetsscheme.WidgetsScheme
state = []
for node in workflow.nodes: # type: SchemeNode
w = workflow.widget_for_node(node)
if w.isVisible():
data = workflow.save_widget_geometry_for_node(node)
state.append((node, data))

presets = workflow.property("_presets") or []
items = [name for name, _ in presets]

dlg = SaveWindowGroup(
self, windowTitle="Save Group as...")
dlg.setWindowModality(Qt.ApplicationModal)
dlg.setItems(items)

menu = self.__windowGroupsAction.menu() # type: QMenu
group = self.__windowGroupsActionGroup

def store_group():
text = dlg.selectedText()
actions = group.actions() # type: List[QAction]
try:
idx = items.index(text)
except ValueError:
idx = -1
newpresets = list(presets)
if idx == -1:
# new group slot
newpresets.append((text, state))
action = QAction(text, menu)
action.setShortcut(
QKeySequence("Meta+P, Ctrl+{}".format(len(newpresets)))
)
oldstate = None
else:
newpresets[idx] = (text, state)
action = actions[idx]
# store old state for undo
_, oldstate = presets[idx]

sep = menu.findChild(QAction, "groups-separator")
assert isinstance(sep, QAction) and sep.isSeparator()

def redo():
action.setData(state)
workflow.setProperty("_presets", newpresets)
if idx == -1:
group.addAction(action)
menu.insertAction(sep, action)

def undo():
action.setData(oldstate)
workflow.setProperty("_presets", presets)
if idx == -1:
group.removeAction(action)
menu.removeAction(action)
if idx == -1:
text = "Store Window Group"
else:
text = "Update Window Group"
self.__undoStack.push(
commands.SimpleUndoCommand(redo, undo, text)
)
dlg.accepted.connect(store_group)
dlg.show()
dlg.raise_()

def __activateWindowGroup(self, action):
# type: (QAction) -> None
state = action.data()
workflow = self.__scheme
if not isinstance(workflow, widgetsscheme.WidgetsScheme):
return

state = {node: geom for node, geom in state}
for node in workflow.nodes:
w = workflow.widget_for_node(node) # type: QWidget
w.setVisible(node in state)
if node in state:
workflow.restore_widget_geometry_for_node(node, state[node])
w.raise_()

def __clearWindowGroups(self):
workflow = self.__scheme
presets = workflow.property("_presets") or []
menu = self.__windowGroupsAction.menu() # type: QMenu
group = self.__windowGroupsActionGroup
actions = group.actions()

def redo():
workflow.setProperty("_presets", [])
for action in reversed(actions):
group.removeAction(action)
menu.removeAction(action)

def undo():
workflow.setProperty("_presets", presets)
sep = menu.findChild(QAction, "groups-separator")
for action in actions:
group.addAction(action)
menu.insertAction(sep, action)

self.__undoStack.push(
commands.SimpleUndoCommand(redo, undo, "Delete All Window Groups")
)


class SaveWindowGroup(QDialog):
"""
A dialog for saving window groups.

The user can select an existing group to overwrite or enter a new group
name.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout()
form = QFormLayout(
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
layout.addLayout(form)
self._combobox = cb = QComboBox(
editable=True, minimumContentsLength=16,
sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
insertPolicy=QComboBox.NoInsert,
)
# default text if no items are present
cb.setEditText(self.tr("Window Group 1"))
cb.lineEdit().selectAll()
form.addRow(self.tr("Save As:"), cb)
bb = QDialogButtonBox(
standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
bb.accepted.connect(self.__accept_check)
bb.rejected.connect(self.reject)
layout.addWidget(bb)
layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
self.setLayout(layout)
self.setWhatsThis(
"Save the current open widgets' window arrangement to the "
"workflow view presets."
)
cb.setFocus(Qt.NoFocusReason)

def __accept_check(self):
cb = self._combobox
text = cb.currentText()
if cb.findText(text) == -1:
self.accept()
return
# Ask for overwrite confirmation
mb = QMessageBox(
self, windowTitle=self.tr("Confirm Overwrite"),
icon=QMessageBox.Question,
standardButtons=QMessageBox.Yes | QMessageBox.Cancel,
text=self.tr("The window group '{}' already exists. Do you want " +
"to replace it?").format(text),
)
mb.setDefaultButton(QMessageBox.Yes)
mb.setEscapeButton(QMessageBox.Cancel)
mb.setWindowModality(Qt.WindowModal)
button = mb.button(QMessageBox.Yes)
button.setText(self.tr("Replace"))
mb.finished.connect(
lambda status: status == QMessageBox.Yes and self.accept()
)
mb.show()

def setItems(self, items):
# type: (List[str]) -> None
"""Set a list of existing items/names to present to the user"""
self._combobox.clear()
self._combobox.addItems(items)
if items:
self._combobox.setCurrentIndex(len(items) - 1)

def selectedText(self):
# type: () -> str
"""Return the current entered text."""
return self._combobox.currentText()


def geometry_from_annotation_item(item):
if isinstance(item, items.ArrowAnnotation):
Expand Down
Loading