"""
This class implements a tree area widget that actually displays and manipulates
phylogenetic trees.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Piotr Rotkiewicz
from past.utils import old_div
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from . import constants
from . import utils
[docs]class TreeArea(QtWidgets.QWidget):
"""
This class implements a tree area widget that performs actual display
and manipulation of a neighbor joining tree associated with the sequences.
"""
[docs] def __init__(self, parent=None):
# Initialize base class.
QtWidgets.QWidget.__init__(self, parent)
self.viewer = None # Parent sequence viewer widget
self.font_size = 11 # of widget
self.setMinimumWidth(100) #after resizing below the widget collapses.
#: In current implementation all branches have identical lengths.
self.branch_length = 0 # of Tree
self.margin = 0 #Distance between widget edges and the tree.
self.setMouseTracking(True) # for tooltips.
[docs] def setViewer(self, viewer):
"""
Set parent widget.
:type viewer: `SequenceViewer`
:param viewer: parent sequence viewer widget
"""
self.viewer = viewer
[docs] def mouseMoveEvent(self, event):
"""
Handles Qt mouse move event.
:type event: QMouseMoveEvent
:param event: mouse move event
"""
if not self.viewer.has_tooltips:
return
x = event.x()
y = event.y()
if self.viewer.sequence_group.tree:
node = self.viewer.sequence_group.tree.findNode(x, y, self.box_size)
self.showToolTip(event.globalPos(), node)
[docs] def mousePressEvent(self, event):
"""
Handles Qt mouse press event.
:type event: QMousePressEvent
:param event: mouse press event
"""
x = event.x()
y = event.y()
# Initially, reset the clicked node.
self.clicked_node = None
# Find the node the user clicked on.
if self.viewer.sequence_group.tree:
self.clicked_node = self.viewer.sequence_group.tree.findNode(
x, y, self.box_size)
# If right mouse button was pressed, just popup a menu and quit.
if event.buttons() & QtCore.Qt.RightButton:
if self.viewer.tree_popup_menu:
self.viewer.tree_popup_menu(event.globalPos(),
self.clicked_node)
return
# Left mouse button action depends on a current mode.
if event.buttons() & QtCore.Qt.LeftButton:
if not (event.modifiers() & QtCore.Qt.ControlModifier):
self.viewer.sequence_group.unselectAllSequences()
if self.clicked_node:
self.swapBranches()
return
# Repaint the viewer.
self.viewer.repaint()
[docs] def swapBranches(self):
"""
This method swaps branches of a clicked node.
"""
if self.clicked_node:
# Swap branches.
self.clicked_node.swap()
# Change sequence order according to the tree.
self.viewer.sequence_group.sortByTreeOrder()
# Sequence order has changed, so we need to re-generate rows.
self.viewer.generateRows()
# Repaint the viewer.
self.viewer.repaint()
# Move mouse cursor to the new node position.
x, y = self.clicked_node.original_node.coordinates
widget_pos = self.mapToGlobal(self.pos())
x += widget_pos.x()
y += widget_pos.y()
QtGui.QCursor.setPos(x, y)
# Draw tooltip.
self.showToolTip(QtCore.QPoint(x, y), self.clicked_node)
[docs] def selectSequences(self):
"""
Selects sequences that belong to a clicked node.
"""
if self.clicked_node:
# Recursively select sequences of the clicked node.
self.clicked_node.selectSequences()
# Repaint the viewer.
self.viewer.repaint()
[docs] def hideSequences(self):
"""
Hides sequences that belong to the clicked node.
"""
if self.clicked_node:
# Recursively hide sequences of the clicked node.
self.clicked_node.hideSequences()
# Regenerate rows.
self.viewer.generateRows()
# Repaint the viewer.
self.viewer.repaint()
[docs] def drawTree(self, painter, tree, position, level):
"""
Draws the tree using a specified painter at a given vertical position.
:type painter: QPainter
:param painter: target painter surface
:type tree: `TreeNode`
:param tree: tree to be drawn
:type position: int
:param position: vertical tree position in painter coordinates
:type level: int
:param level: drawing recursion level
"""
n_branches = 0
avg_position = 0
min_position = self.height()
max_position = 0
for branch in tree.branches:
if len(branch.branches) > 0:
position, branch_position, min_pos, max_pos = self.drawTree(
painter, branch, position, level + 1)
x_pos = self.margin + branch.level * \
self.branch_length - self.branch_length
painter.drawLine(x_pos, branch_position,
x_pos + self.branch_length, branch_position)
painter.drawLine(x_pos + self.branch_length, min_pos,
x_pos + self.branch_length, max_pos)
rx = x_pos + self.branch_length
ry = branch_position
branch.original_node.coordinates = None
if min_pos < max_pos:
# Store the node coordinates for mouse interaction.
branch.original_node.coordinates = (rx, ry)
# Draw a node box.
painter.drawEllipse(QtCore.QPoint(rx, ry),
self.half_box_size, self.half_box_size)
if branch_position < min_position:
min_position = branch_position
if branch_position > max_position:
max_position = branch_position
n_branches += 1
avg_position += branch_position
else:
# This is a leaf.
x_pos = self.margin + tree.level * self.branch_length
painter.drawLine(x_pos, position,
self.width() - self.margin, position)
n_branches += 1
avg_position += position
if position < min_position:
min_position = position
if position > max_position:
max_position = position
position += self.viewer.font_height
if not branch.sequences[0].collapsed:
if not self.viewer.group_annotations:
for child in branch.sequences[0].children:
if child.visible:
position += self.viewer.font_height
if n_branches > 0:
avg_position /= n_branches
if level == 0:
rx = self.margin
ry = 0.5 * (min_position + max_position)
tree.original_node.coordinates = (rx, ry)
painter.drawLine(x_pos, min_position, x_pos, max_position)
painter.drawEllipse(QtCore.QPoint(rx, ry), self.half_box_size,
self.half_box_size)
return position, avg_position, min_position, max_position
[docs] def paintEvent(self, event):
"""
Handles Qt paint event for the `TreeArea`.
:type event: QPaintEvent
:param event: Qt paint event
"""
painter = QtGui.QPainter(self)
try:
self.paintTreeArea(painter, event)
except:
# This should never happen, however, just to be safe
# we ignore the painting exception. Uncomment the line below
# for debugging.
if constants.MSV_DEBUG:
utils.print_error("Tree area painter exception.")
[docs] def paintTreeArea(self, painter, event, clip=True):
"""
Actually paints the tree area on a specified painter.
:type painter: QPainter
:param painter: painter used to draw the tree area
:type event: QPaintEvent
:param event: Qt paint event
"""
rows = self.viewer.rows
font_height = self.viewer.font_height
font_xheight = self.viewer.font_xheight
self.half_box_size = int(self.viewer.font_height * 0.125) + 1
if self.half_box_size % 2:
self.half_box_size += 1
self.box_size = self.half_box_size * 2
width = self.width()
height = self.height()
painter.setPen(QtCore.Qt.white)
painter.setBrush(QtGui.QBrush(QtCore.Qt.white))
if clip:
painter.setClipRect(0, 0, width - 1, height - 1)
painter.drawRect(0, 0, width, height)
y_position = self.viewer.margin
painter.setPen(QtCore.Qt.black)
tree = self.viewer.sequence_group.tree
if not tree:
return
self.margin = self.viewer.margin + 5
separator_pen = QtGui.QPen(QtCore.Qt.gray)
separator_pen.setStyle(QtCore.Qt.DotLine)
self.branch_length = \
old_div((self.width() - 2 * self.margin), float(tree.maxLevel() + 1))
self.tree_line_pen = QtGui.QPen(QtCore.Qt.black)
self.tree_line_pen.setStyle(QtCore.Qt.SolidLine)
self.node_box_brush = QtGui.QBrush(QtCore.Qt.black)
tree_drawn = False
for row_idx, row in enumerate(rows):
if row_idx < self.viewer.top_row and clip:
continue
if y_position > height and clip:
break
# Display a single row.
seq, start, end, row_height = row
# Display the sequence only if it is visible.
if seq and seq.visible:
if seq.type == constants.SEQ_AMINO_ACIDS:
if tree and not tree_drawn:
painter.setPen(self.tree_line_pen)
painter.setBrush(self.node_box_brush)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
visible_tree = tree.getVisibleTree()
self.drawTree(painter, visible_tree,
y_position + 0.5 * font_height, 0)
painter.setRenderHint(QtGui.QPainter.Antialiasing,
False)
tree_drawn = True
if seq.type == constants.SEQ_SEPARATOR:
painter.setPen(separator_pen)
painter.drawLine(
0, y_position + font_height - font_xheight - 2, width,
y_position + font_height - font_xheight - 2)
tree_drawn = False
y_position = y_position + font_height * row_height