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 Annotations: Text markup editing #2422

Merged
merged 5 commits into from
Jun 30, 2017
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
314 changes: 274 additions & 40 deletions Orange/canvas/canvas/items/annotationitem.py

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions Orange/canvas/canvas/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,16 +550,15 @@ def add_annotation(self, scheme_annot):

if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
item = items.TextAnnotation()
item.setPlainText(scheme_annot.text)
x, y, w, h = scheme_annot.rect
item.setPos(x, y)
item.resize(w, h)
item.setTextInteractionFlags(Qt.TextEditorInteraction)

font = font_from_dict(scheme_annot.font, item.font())
item.setFont(font)
scheme_annot.text_changed.connect(item.setPlainText)

item.setContent(scheme_annot.content, scheme_annot.content_type)
scheme_annot.content_changed.connect(item.setContent)
elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
item = items.ArrowAnnotation()
start, end = scheme_annot.start_pos, scheme_annot.end_pos
Expand Down Expand Up @@ -597,10 +596,7 @@ def remove_annotation(self, scheme_annotation):
)

if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
scheme_annotation.text_changed.disconnect(
item.setPlainText
)

scheme_annotation.content_changed.disconnect(item.setContent)
self.remove_annotation_item(item)

def annotation_items(self):
Expand Down Expand Up @@ -813,7 +809,7 @@ def mousePressEvent(self, event):

# Right (context) click on the node item. If the widget is not
# in the current selection then select the widget (only the widget).
# Else simply return and let customContextMenuReqested signal
# Else simply return and let customContextMenuRequested signal
# handle it
shape_item = self.item_at(event.scenePos(), items.NodeItem)
if shape_item and event.button() == Qt.RightButton and \
Expand Down Expand Up @@ -856,6 +852,12 @@ def keyReleaseEvent(self, event):
return
return QGraphicsScene.keyReleaseEvent(self, event)

def contextMenuEvent(self, event):
if self.user_interaction_handler and \
self.user_interaction_handler.contextMenuEvent(event):
return
super().contextMenuEvent(event)

def set_user_interaction_handler(self, handler):
if self.user_interaction_handler and \
not self.user_interaction_handler.isFinished():
Expand Down
14 changes: 9 additions & 5 deletions Orange/canvas/document/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,22 @@ def undo(self):


class TextChangeCommand(QUndoCommand):
def __init__(self, scheme, annotation, old, new, parent=None):
def __init__(self, scheme, annotation,
old_content, old_content_type,
new_content, new_content_type, parent=None):
QUndoCommand.__init__(self, "Change text", parent)
self.scheme = scheme
self.annotation = annotation
self.old = old
self.new = new
self.old_content = old_content
self.old_content_type = old_content_type
self.new_content = new_content
self.new_content_type = new_content_type

def redo(self):
self.annotation.text = self.new
self.annotation.set_content(self.new_content, self.new_content_type)

def undo(self):
self.annotation.text = self.old
self.annotation.set_content(self.old_content, self.old_content_type)


class SetAttrCommand(QUndoCommand):
Expand Down
10 changes: 8 additions & 2 deletions Orange/canvas/document/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ def keyReleaseEvent(self, event):
"""
return False

def contextMenuEvent(self, event):
"""
Handle a `QGraphicsScene.contextMenuEvent`
"""
return False


class NoPossibleLinksError(ValueError):
pass
Expand Down Expand Up @@ -1262,7 +1268,7 @@ def __init__(self, document, *args, **kwargs):

def mousePressEvent(self, event):
pos = event.scenePos()
if self.item is None:
if event.button() & Qt.LeftButton and self.item is None:
item = self.scene.item_at(pos, items.TextAnnotation)
if item is not None and not item.hasFocus():
self.editItem(item)
Expand Down Expand Up @@ -1317,7 +1323,7 @@ def __on_textGeometryChanged(self):
self.control.setRect(rect)

def cancel(self, reason=UserInteraction.OtherReason):
log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
log.debug("ResizeTextAnnotation.cancel(%s)", reason)
if self.item is not None and self.savedRect is not None:
self.item.setGeometry(self.savedRect)

Expand Down
73 changes: 37 additions & 36 deletions Orange/canvas/document/schemeedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,6 @@ def __setupUi(self):
view = CanvasView(scene)
view.setFrameStyle(CanvasView.NoFrame)
view.setRenderHint(QPainter.Antialiasing)
view.setContextMenuPolicy(Qt.CustomContextMenu)
view.customContextMenuRequested.connect(
self.__onCustomContextMenuRequested
)

self.__view = view
self.__scene = scene
Expand Down Expand Up @@ -951,7 +947,7 @@ def eventFilter(self, obj, event):
# Filter the scene's drag/drop events.
if obj is self.scene():
etype = event.type()
if etype == QEvent.GraphicsSceneDragEnter or \
if etype == QEvent.GraphicsSceneDragEnter or \
etype == QEvent.GraphicsSceneDragMove:
mime_data = event.mimeData()
if mime_data.hasFormat(
Expand Down Expand Up @@ -1185,6 +1181,32 @@ def sceneKeyReleaseEvent(self, event):
return False

def sceneContextMenuEvent(self, event):
scenePos = event.scenePos()
globalPos = event.screenPos()

item = self.scene().item_at(scenePos, items.NodeItem)
if item is not None:
self.__widgetMenu.popup(globalPos)
return True

item = self.scene().item_at(scenePos, items.LinkItem,
buttons=Qt.RightButton)
if item is not None:
link = self.scene().link_for_item(item)
self.__linkEnableAction.setChecked(link.enabled)
self.__contextMenuTarget = link
self.__linkMenu.popup(globalPos)
return True

item = self.scene().item_at(scenePos)
if not item and \
self.__quickMenuTriggers & SchemeEditWidget.RightClicked:
action = interactions.NewNodeAction(self)

with disabled(self.__undoAction), disabled(self.__redoAction):
action.create_new(globalPos)
return True

return False

def _setUserInteractionHandler(self, handler):
Expand Down Expand Up @@ -1337,11 +1359,17 @@ def __onEditingFinished(self, item):
Text annotation editing has finished.
"""
annot = self.__scene.annotation_for_item(item)
text = str(item.toPlainText())
if annot.text != text:

content_type = item.contentType()
content = item.content()

if annot.text != content or annot.content_type != content_type:
self.__undoStack.push(
commands.TextChangeCommand(self.scheme(), annot,
annot.text, text)
commands.TextChangeCommand(
self.scheme(), annot,
annot.text, annot.content_type,
content, content_type
)
)

def __toggleNewArrowAnnotation(self, checked):
Expand Down Expand Up @@ -1416,33 +1444,6 @@ def __onArrowColorTriggered(self, action):
if isinstance(handler, interactions.NewArrowAnnotation):
handler.setColor(action.data())

def __onCustomContextMenuRequested(self, pos):
scenePos = self.view().mapToScene(pos)
globalPos = self.view().mapToGlobal(pos)

item = self.scene().item_at(scenePos, items.NodeItem)
if item is not None:
self.__widgetMenu.popup(globalPos)
return

item = self.scene().item_at(scenePos, items.LinkItem,
buttons=Qt.RightButton)
if item is not None:
link = self.scene().link_for_item(item)
self.__linkEnableAction.setChecked(link.enabled)
self.__contextMenuTarget = link
self.__linkMenu.popup(globalPos)
return

item = self.scene().item_at(scenePos)
if not item and \
self.__quickMenuTriggers & SchemeEditWidget.RightClicked:
action = interactions.NewNodeAction(self)

with disabled(self.__undoAction), disabled(self.__redoAction):
action.create_new(globalPos)
return

def __onRenameAction(self):
"""
Rename was requested for the selected widget.
Expand Down
60 changes: 53 additions & 7 deletions Orange/canvas/scheme/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,21 @@ class SchemeTextAnnotation(BaseSchemeAnnotation):
Text annotation in the scheme.
"""

# Signal emitted when the annotation content change.
content_changed = Signal(str, str)

# Signal emitted when the annotation text changes.
text_changed = Signal(str)

# Signal emitted when the annotation text font changes.
font_changed = Signal(dict)

def __init__(self, rect, text="", font=None, anchor=None, parent=None):
def __init__(self, rect, text="", content_type="text/plain", font=None,
anchor=None, parent=None):
BaseSchemeAnnotation.__init__(self, parent)
self.__rect = rect
self.__text = text
self.__content = text
self.__content_type = content_type
self.__font = {} if font is None else font
self.__anchor = anchor

Expand Down Expand Up @@ -150,24 +155,65 @@ def geometry(self):
def set_text(self, text):
"""
Set the annotation text.

Same as `set_content(text, "text/plain")`
"""
check_type(text, str)
text = str(text)
if self.__text != text:
self.__text = text
self.text_changed.emit(text)
self.set_content(text, "text/plain")

def text(self):
"""
Annotation text.

.. deprecated::
Use `content` instead.
"""
return self.__text
return self.__content

text = Property(tuple, fget=text, fset=set_text)

@property
def content_type(self):
"""
Return the annotations' content type.

Currently this will be 'text/plain', 'text/html' or 'text/rst'.
"""
return self.__content_type

@property
def content(self):
"""
The annotation content.

How the content is interpreted/displayed depends on `content_type`.
"""
return self.__content

def set_content(self, content, content_type="text/plain"):
"""
Set the annotation content.

Parameters
----------
content : str
The content.
content_type : str
Content type. Currently supported are 'text/plain' 'text/html'
(subset supported by `QTextDocument`) and `text/rst`.
"""
if self.__content != content or self.__content_type != content_type:
text_changed = self.__content != content
self.__content = content
self.__content_type = content_type
self.content_changed.emit(content, content_type)
if text_changed:
self.text_changed.emit(content)

def set_font(self, font):
"""
Set the annotation's font as a dictionary of font properties
Set the annotation's default font as a dictionary of font properties
(at the moment only family and size are used).

>>> annotation.set_font({"family": "Helvetica", "size": 16})
Expand Down
36 changes: 14 additions & 22 deletions Orange/canvas/scheme/readwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,7 @@ def terminal_eval(source):

"""
node = ast.parse(source, "<source>", mode="eval")

try:
return _terminal_value(node.body)
except ValueError:
raise
raise ValueError("%r is not a terminal constant" % source)
return _terminal_value(node.body)


def _terminal_value(node):
Expand Down Expand Up @@ -426,7 +421,7 @@ def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None,

_text_params = namedtuple(
"_text_params",
["geometry", "text", "font"])
["geometry", "text", "font", "content_type"])

_arrow_params = namedtuple(
"_arrow_params",
Expand Down Expand Up @@ -487,10 +482,13 @@ def parse_ows_etree_v_2_0(tree):
if font_size:
font["size"] = int(font_size)

content_type = annot.get("type", "text/plain")

annotation = _annotation(
id=annot.get("id"),
type="text",
params=_text_params(rect, annot.text or "", font),
params=_text_params(rect, annot.text or "", font,
content_type),
)
elif annot.tag == "arrow":
start = tuple_eval(annot.get("start", "(0, 0)"))
Expand Down Expand Up @@ -727,8 +725,10 @@ def error_handler(exc):
for annot_d in desc.annotations:
params = annot_d.params
if annot_d.type == "text":
annot = SchemeTextAnnotation(params.geometry, params.text,
params.font)
annot = SchemeTextAnnotation(
params.geometry, params.text, params.content_type,
params.font
)
elif annot_d.type == "arrow":
start, end = params.geometry
annot = SchemeArrowAnnotation(start, end, params.color)
Expand Down Expand Up @@ -818,6 +818,7 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False):
data = None
if isinstance(annotation, SchemeTextAnnotation):
tag = "text"
attrs.update({"type": annotation.content_type})
attrs.update({"rect": repr(annotation.rect)})

# Save the font attributes
Expand All @@ -827,21 +828,12 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False):
attrs = [(key, value) for key, value in attrs.items()
if value is not None]
attrs = dict((key, str(value)) for key, value in attrs)

data = annotation.text

data = annotation.content
elif isinstance(annotation, SchemeArrowAnnotation):
tag = "arrow"
attrs.update({"start": repr(annotation.start_pos),
"end": repr(annotation.end_pos)})

# Save the arrow color
try:
color = annotation.color
attrs.update({"fill": color})
except AttributeError:
pass

"end": repr(annotation.end_pos),
"fill": annotation.color})
data = None
else:
log.warning("Can't save %r", annotation)
Expand Down
Loading