././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9810882
xdot-1.6/ 0000755 0001751 0001751 00000000000 15115636633 012015 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/LICENSE.txt 0000644 0001751 0001751 00000016743 15115636571 013654 0 ustar 00runner runner GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9810882
xdot-1.6/PKG-INFO 0000644 0001751 0001751 00000002260 15115636633 013112 0 ustar 00runner runner Metadata-Version: 2.1
Name: xdot
Version: 1.6
Summary: Interactive viewer for Graphviz dot files
Home-page: https://github.com/jrfonseca/xdot.py
Author: Jose Fonseca
Author-email: jose.r.fonseca@gmail.com
License: LGPL
Platform: UNKNOWN
Classifier: Development Status :: 6 - Mature
Classifier: Environment :: X11 Applications :: GTK
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: OS Independent
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Multimedia :: Graphics :: Viewers
License-File: LICENSE.txt
xdot.py is an interactive viewer for graphs written in Graphviz's dot
language.
It uses internally the graphviz's xdot output format as an intermediate
format, and PyGTK and Cairo for rendering.
xdot.py can be used either as a standalone application from command
line, or as a library embedded in your python application.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/README.md 0000644 0001751 0001751 00000012761 15115636571 013304 0 ustar 00runner runner About _xdot.py_
=================
_xdot.py_ is an interactive viewer for graphs written in [Graphviz](http://www.graphviz.org/)'s [dot language](http://www.graphviz.org/doc/info/lang.html).
It uses internally the GraphViz's [xdot output format](http://www.graphviz.org/doc/info/output.html#d:xdot) as an intermediate format, [Python GTK bindings](https://pygobject.readthedocs.io), and [Cairo](https://cairographics.org/) for rendering.
_xdot.py_ can be used either as a standalone application from command line, or as a library embedded in your Python application.
Status
======
_xdot.py_ script became much more popular than I ever anticipated, and there are several interested in improving it further. However, for several years now, _xdot.py_ already meets my own needs, and unfortunately I don't have much time for maintaining it myself.
So I'm looking into transition _xdot.py_ maintenance to [others](https://github.com/jrfonseca/xdot.py/wiki/Forks): either hand over the maintenance _xdot.py_ to a community or indicate an official fork of _xdot.py_.
I encourage people interested in development of _xdot.py_ to fork the [GitHub repository](https://github.com/jrfonseca/xdot.py), and join the new [mailing list](https://groups.google.com/d/forum/xdot-py).
Features
========
* Since it doesn't use bitmaps it is fast and has a small memory footprint.
* Arbitrary zoom.
* Keyboard/mouse navigation.
* Supports events on the nodes with URLs.
* Animated jumping between nodes.
* Highlights node/edge under mouse.
Known Issues
============
* Not all xdot attributes are supported or correctly rendered yet. It works well for my applications but YMMV.
* Text doesn't scale properly to large sizes if font hinting is enabled. I haven't found a reliable way to disable font hinting during rendering yet.
See also:
* [github issue tracker](https://github.com/jrfonseca/xdot.py/issues)
Screenshots
===========
[](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-profile1.png)
[](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-profile2.png)
[](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-cfg.png)
Requirements
============
* [Python 3](https://www.python.org/download/)
* [PyGObject bindings for GTK3](https://pygobject.readthedocs.io)
* [NumPy](https://numpy.org/)
* [Graphviz](https://graphviz.org/download/)
Windows users
-------------
Download and install:
* [Python for Windows](https://www.python.org/downloads/windows/)
* [PyGObject bindings for GTK3](https://pygobject.readthedocs.io/en/latest/getting_started.html#windows-getting-started)
* `pip install numpy`
* [Graphviz for Windows](https://graphviz.org/download/)
Debian/Ubuntu users
-------------------
Run:
apt install gir1.2-gtk-3.0 python3-gi python3-gi-cairo python3-numpy graphviz
Usage
=====
Command Line
------------
If you install _xdot.py_ from PyPI or from your Linux distribution package managing system, you should have the `xdot` somewhere in your `PATH` automatically.
When running _xdot.py_ from its source tree, you can run it by first setting `PYTHONPATH` environment variable to the full path of _xdot.py_'s source tree, then running:
python3 -m xdot
You can also pass the following options:
Usage:
xdot.py [file|-]
Options:
-h, --help show this help message and exit
-f FILTER, --filter=FILTER
graphviz filter: dot, neato, twopi, circo, or fdp
[default: dot]
-n, --no-filter assume input is already filtered into xdot format (use
e.g. dot -Txdot)
-g GEOMETRY default window size in form WxH
Shortcuts:
Up, Down, Left, Right scroll
PageUp, +, = zoom in
PageDown, - zoom out
R reload dot file
F find
Q quit
P print
T toggle toolbar
W zoom to fit
Escape halt animation
Ctrl-drag zoom in/out
Shift-drag zooms an area
Click (on edge) focus edge's source node
Ctrl-click (on edge) focus edge's *destination* node
If `-` is given as input file then _xdot.py_ will read the dot graph from the standard input.
Embedding
---------
See included `sample.py` script for an example of how to embedded _xdot.py_ into another application.
[](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-sample.png)
Download
========
* https://pypi.python.org/pypi/xdot
* https://github.com/jrfonseca/xdot.py
Links
=====
* [Graphviz homepage](http://www.graphviz.org/)
* [ZGRViewer](http://zvtm.sourceforge.net/zgrviewer.html) -- another superb graphviz/dot viewer
* [dot2tex](https://github.com/kjellmf/dot2tex) -- python script to convert xdot output from Graphviz to a series of PSTricks or PGF/TikZ commands.
* The [PyPy project](http://pypy.org/) also includes an [interactive dot viewer based on graphviz's plain format and the pygame library](https://morepypy.blogspot.com/2008/01/visualizing-python-tokenizer.html).
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9810882
xdot-1.6/setup.cfg 0000644 0001751 0001751 00000000046 15115636633 013636 0 ustar 00runner runner [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/setup.py 0000755 0001751 0001751 00000003451 15115636571 013536 0 ustar 00runner runner #!/usr/bin/env python3
#
# The purpose of this script is to enable uploading xdot.py to the Python
# Package Index, which can be easily done by doing:
#
# python3 setup.py sdist upload
#
# See also:
# - https://packaging.python.org/distributing/
# - https://docs.python.org/3/distutils/packageindex.html
#
from setuptools import setup
setup(
name='xdot',
version='1.6',
author='Jose Fonseca',
author_email='jose.r.fonseca@gmail.com',
url='https://github.com/jrfonseca/xdot.py',
description="Interactive viewer for Graphviz dot files",
long_description="""
xdot.py is an interactive viewer for graphs written in Graphviz's dot
language.
It uses internally the graphviz's xdot output format as an intermediate
format, and PyGTK and Cairo for rendering.
xdot.py can be used either as a standalone application from command
line, or as a library embedded in your python application.
""",
license="LGPL",
packages=['xdot', 'xdot/dot', 'xdot/ui'],
entry_points=dict(gui_scripts=['xdot=xdot.__main__:main']),
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 6 - Mature',
'Environment :: X11 Applications :: GTK',
'Intended Audience :: Information Technology',
'Operating System :: OS Independent',
'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Multimedia :: Graphics :: Viewers',
],
install_requires=[
'PyGObject',
'numpy',
'packaging',
],
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9780881
xdot-1.6/xdot/ 0000755 0001751 0001751 00000000000 15115636633 012773 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/__init__.py 0000644 0001751 0001751 00000003020 15115636571 015100 0 ustar 00runner runner #
# Copyright 2008-2022 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
'''Visualize dot graphs via the xdot format.'''
import sys
assert sys.version_info.major >= 3
import importlib
import warnings
__all__ = ['dot', 'ui']
__author__ = "Jose Fonseca et al"
# Leverage PEP 562 to maintain backwards compatibility while still
# allowing xdot.dot to be used headlessly.
if sys.version_info >= (3, 7):
def __getattr__(name):
if name in ('dot', 'ui'):
return importlib.import_module("." + name, __name__)
if name in ('DotWidget', 'DotWindow'):
warnings.warn(f"w xdot.{name} is deprecated, use xdot.ui.{name} instead", UserWarning, stacklevel=2)
from . import ui
return getattr(ui, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
else:
from . import dot
from . import ui
from .ui import DotWidget, DotWindow
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/__main__.py 0000755 0001751 0001751 00000006457 15115636571 015105 0 ustar 00runner runner #!/usr/bin/env python3
#
# Copyright 2008-2017 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import argparse
import sys
from .ui.window import DotWindow, Gtk
def main():
parser = argparse.ArgumentParser(
description="xdot.py is an interactive viewer for graphs written in Graphviz's dot language.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Shortcuts:
Up, Down, Left, Right scroll
PageUp, +, = zoom in
PageDown, - zoom out
R reload dot file
F find
Q quit
P print
T toggle show/hide toolbar
Escape halt animation
Ctrl-drag zoom in/out
Shift-drag zooms an area
Click (on edge) focus edge's source node
Ctrl-click (on edge) focus edge's *destination* node
'''
)
parser.add_argument(
'inputfile', metavar='file', nargs='?',
help='input file to be viewed')
parser.add_argument(
'-f', '--filter', choices=['dot', 'neato', 'twopi', 'circo', 'fdp'],
dest='filter', default='dot', metavar='FILTER',
help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %(default)s]')
parser.add_argument(
'-n', '--no-filter',
action='store_const', const=None, dest='filter',
help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
parser.add_argument(
'-g', '--geometry',
action='store', dest='geometry',
help='default window size in form WxH')
parser.add_argument(
'--hide-toolbar',
action='store_true', dest='hide_toolbar',
help='Hides the toolbar on start.')
options = parser.parse_args()
inputfile = options.inputfile
width, height = 610, 610
if options.geometry:
try:
width, height = (int(i) for i in options.geometry.split('x'))
except ValueError:
parser.error('invalid window geometry')
win = DotWindow(width=width, height=height)
win.connect('delete-event', Gtk.main_quit)
win.set_filter(options.filter)
if inputfile and len(inputfile) >= 1:
if inputfile == '-':
win.set_dotcode(sys.stdin.buffer.read())
else:
win.open_file(inputfile)
if options.hide_toolbar:
win.uimanager.get_widget('/ToolBar').set_visible(False)
if sys.platform != 'win32':
# Reset KeyboardInterrupt SIGINT handler, so that glib loop can be stopped by it
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
Gtk.main()
if __name__ == '__main__':
main()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9790883
xdot-1.6/xdot/dot/ 0000755 0001751 0001751 00000000000 15115636633 013561 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/dot/__init__.py 0000644 0001751 0001751 00000000036 15115636571 015672 0 ustar 00runner runner __all__ = ['lexer', 'parser']
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/dot/lexer.py 0000644 0001751 0001751 00000010500 15115636571 015247 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import os
import re
from .scanner import DotScanner
EOF = -1
SKIP = -2
ID = 0
STR_ID = 1
HTML_ID = 2
class Token:
def __init__(self, type, text, line, col):
self.type = type
self.text = text
self.line = line
self.col = col
class ParseError(Exception):
def __init__(self, msg=None, filename=None, line=None, col=None):
self.msg = msg
self.filename = filename
self.line = line
self.col = col
def __str__(self):
return ':'.join([str(part) for part in
(self.filename, self.line, self.col, self.msg)
if part is not None])
class Lexer:
# should be overriden by derived classes
scanner = None
tabsize = 8
newline_re = re.compile(br'\r\n?|\n')
def __init__(self, buf=None, pos=0, filename=None, fp=None):
if fp is not None:
try:
fileno = fp.fileno()
length = os.path.getsize(fp.name)
import mmap
except:
# read whole file into memory
buf = fp.read()
pos = 0
else:
# map the whole file into memory
if length:
# length must not be zero
buf = mmap.mmap(fileno, length, access=mmap.ACCESS_READ)
pos = os.lseek(fileno, 0, 1)
else:
buf = b''
pos = 0
if filename is None:
try:
filename = fp.name
except AttributeError:
filename = None
self.buf = buf
self.pos = pos
self.line = 1
self.col = 1
self.filename = filename
def __next__(self):
while True:
# save state
pos = self.pos
line = self.line
col = self.col
type, text, endpos = self.scanner.next(self.buf, pos)
assert isinstance(text, bytes)
assert pos + len(text) == endpos
self.consume(text)
type, text = self.filter(type, text)
self.pos = endpos
if type == SKIP:
continue
elif type is None:
msg = 'unexpected char %r' % (text,)
raise ParseError(msg, self.filename, line, col)
else:
break
return Token(type=type, text=text, line=line, col=col)
def consume(self, text):
# update line number
pos = 0
for mo in self.newline_re.finditer(text, pos):
self.line += 1
self.col = 1
pos = mo.end()
# update column number
while True:
tabpos = text.find(b'\t', pos)
if tabpos == -1:
break
self.col += tabpos - pos
self.col = ((self.col - 1) // self.tabsize + 1) * self.tabsize + 1
pos = tabpos + 1
self.col += len(text) - pos
class DotLexer(Lexer):
scanner = DotScanner()
def filter(self, type, text):
# TODO: handle charset
if type == STR_ID:
text = text[1:-1]
# line continuations
text = text.replace(b'\\\r\n', b'')
text = text.replace(b'\\\r', b'')
text = text.replace(b'\\\n', b'')
# quotes
text = text.replace(b'\\"', b'"')
# layout engines recognize other escape codes (many non-standard)
# but we don't translate them here
type = ID
elif type == HTML_ID:
text = text[1:-1]
type = ID
return type, text
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/dot/parser.py 0000644 0001751 0001751 00000013654 15115636571 015441 0 ustar 00runner runner # Copyright 2008-2022 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import sys
from .lexer import ParseError, DotLexer
EOF = -1
SKIP = -2
ID = 0
STR_ID = 1
HTML_ID = 2
EDGE_OP = 3
LSQUARE = 4
RSQUARE = 5
LCURLY = 6
RCURLY = 7
COMMA = 8
COLON = 9
SEMI = 10
EQUAL = 11
PLUS = 12
STRICT = 13
GRAPH = 14
DIGRAPH = 15
NODE = 16
EDGE = 17
SUBGRAPH = 18
def __getattr__(name: str):
if name == "XDotParser":
from ..ui._xdotparser import XDotParser
return XDotParser
return AttributeError(f"module {__name__!r} has no attribute {name!r}")
class Parser:
def __init__(self, lexer):
self.lexer = lexer
self.lookahead = next(self.lexer)
def match(self, type):
if self.lookahead.type != type:
raise ParseError(
msg='unexpected token {}'.format(self.lookahead.text),
filename=self.lexer.filename,
line=self.lookahead.line,
col=self.lookahead.col)
def skip(self, type):
while self.lookahead.type != type:
if self.lookahead.type == EOF:
raise ParseError(
msg='unexpected end of file',
filename=self.lexer.filename,
line=self.lookahead.line,
col=self.lookahead.col)
self.consume()
def consume(self):
token = self.lookahead
self.lookahead = next(self.lexer)
return token
class DotParser(Parser):
def parse(self):
self.parse_graph()
if self.lookahead.type != EOF:
# Multiple graphs beyond the first are ignored
# https://github.com/jrfonseca/xdot.py/issues/112
sys.stderr.write('warning: {}:{}:{}: ignoring extra token {}\n'.format(
self.lexer.filename,
self.lookahead.line,
self.lookahead.col,
self.lookahead.text
))
def parse_graph(self):
if self.lookahead.type == STRICT:
self.consume()
self.skip(LCURLY)
self.consume()
while self.lookahead.type != RCURLY:
self.parse_stmt()
self.consume()
def parse_subgraph(self):
id = None
if self.lookahead.type == SUBGRAPH:
self.consume()
if self.lookahead.type == ID:
id = self.lookahead.text
self.consume()
# A subgraph is also a node.
self.handle_node(id, {})
if self.lookahead.type == LCURLY:
self.consume()
while self.lookahead.type != RCURLY:
self.parse_stmt()
self.consume()
return id
def parse_stmt(self):
if self.lookahead.type == GRAPH:
self.consume()
attrs = self.parse_attrs()
self.handle_graph(attrs)
elif self.lookahead.type == NODE:
self.consume()
self.parse_attrs()
elif self.lookahead.type == EDGE:
self.consume()
self.parse_attrs()
elif self.lookahead.type in (SUBGRAPH, LCURLY):
self.parse_subgraph()
else:
id = self.parse_node_id()
if self.lookahead.type == EDGE_OP:
self.consume()
node_ids = [id, self.parse_node_id()]
while self.lookahead.type == EDGE_OP:
self.consume()
node_ids.append(self.parse_node_id())
attrs = self.parse_attrs()
for i in range(0, len(node_ids) - 1):
self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
elif self.lookahead.type == EQUAL:
self.consume()
self.parse_id()
else:
attrs = self.parse_attrs()
self.handle_node(id, attrs)
if self.lookahead.type == SEMI:
self.consume()
def parse_attrs(self):
attrs = {}
while self.lookahead.type == LSQUARE:
self.consume()
while self.lookahead.type != RSQUARE:
name, value = self.parse_attr()
name = name.decode('utf-8')
attrs[name] = value
if self.lookahead.type == COMMA:
self.consume()
self.consume()
return attrs
def parse_attr(self):
name = self.parse_id()
if self.lookahead.type == EQUAL:
self.consume()
value = self.parse_id()
else:
value = b'true'
return name, value
def parse_node_id(self):
node_id = self.parse_id()
if self.lookahead.type == COLON:
self.consume()
port = self.parse_id()
if self.lookahead.type == COLON:
self.consume()
compass_pt = self.parse_id()
else:
compass_pt = None
else:
port = None
compass_pt = None
# XXX: we don't really care about port and compass point
# values when parsing xdot
return node_id
def parse_id(self):
self.match(ID)
id = self.lookahead.text
self.consume()
return id
def handle_graph(self, attrs):
pass
def handle_node(self, id, attrs):
pass
def handle_edge(self, src_id, dst_id, attrs):
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/dot/scanner.py 0000644 0001751 0001751 00000005664 15115636571 015600 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import re
EOF = -1
SKIP = -2
ID = 0
STR_ID = 1
HTML_ID = 2
EDGE_OP = 3
LSQUARE = 4
RSQUARE = 5
LCURLY = 6
RCURLY = 7
COMMA = 8
COLON = 9
SEMI = 10
EQUAL = 11
PLUS = 12
STRICT = 13
GRAPH = 14
DIGRAPH = 15
NODE = 16
EDGE = 17
SUBGRAPH = 18
class Scanner:
"""Stateless scanner."""
# should be overriden by derived classes
tokens = []
symbols = {}
literals = {}
ignorecase = False
def __init__(self):
flags = re.DOTALL
if self.ignorecase:
flags |= re.IGNORECASE
self.tokens_re = re.compile(
b'|'.join([b'(' + regexp + b')'
for type, regexp, test_lit in self.tokens]),
flags
)
def next(self, buf, pos):
if pos >= len(buf):
return EOF, b'', pos
mo = self.tokens_re.match(buf, pos)
if mo:
text = mo.group()
type, regexp, test_lit = self.tokens[mo.lastindex - 1]
pos = mo.end()
if test_lit:
type = self.literals.get(text, type)
return type, text, pos
else:
c = buf[pos:pos+1]
return self.symbols.get(c, None), c, pos + 1
class DotScanner(Scanner):
# token regular expression table
tokens = [
# whitespace and comments
(SKIP,
br'[ \t\f\r\n\v]+|'
br'//[^\r\n]*|'
br'/\*.*?\*/|'
br'#[^\r\n]*',
False),
# Alphanumeric IDs
(ID, br'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
# Numeric IDs
(ID, br'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
# String IDs
(STR_ID, br'"[^"\\]*(?:\\.[^"\\]*)*"', False),
# HTML IDs
(HTML_ID, br'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
# Edge operators
(EDGE_OP, br'-[>-]', False),
]
# symbol table
symbols = {
b'[': LSQUARE,
b']': RSQUARE,
b'{': LCURLY,
b'}': RCURLY,
b',': COMMA,
b':': COLON,
b';': SEMI,
b'=': EQUAL,
b'+': PLUS,
}
# literal table
literals = {
b'strict': STRICT,
b'graph': GRAPH,
b'digraph': DIGRAPH,
b'node': NODE,
b'edge': EDGE,
b'subgraph': SUBGRAPH,
}
ignorecase = True
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9800882
xdot-1.6/xdot/ui/ 0000755 0001751 0001751 00000000000 15115636633 013410 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/__init__.py 0000644 0001751 0001751 00000000426 15115636571 015524 0 ustar 00runner runner __all__ = ['actions', 'animation', 'colors', 'elements', 'pen', 'window']
import sys
try:
import gi
except ImportError:
sys.stderr.write('error: PyGObject bindings for GTK3 not found (https://git.io/JLjeE)\n')
sys.exit(1)
from .window import DotWidget, DotWindow
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/_xdotparser.py 0000644 0001751 0001751 00000036135 15115636571 016325 0 ustar 00runner runner # Copyright 2008-2022 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import colorsys
import re
import sys
from typing import Union
from packaging.version import Version
from ..dot.lexer import DotLexer
from ..dot.parser import DotParser
from ..ui.colors import lookup_color
from ..ui.pen import Pen
from ..ui import elements
class XDotAttrParser:
"""Parser for xdot drawing attributes.
See also:
- http://www.graphviz.org/doc/info/output.html#d:xdot
"""
def __init__(self, parser, buf, broken_backslashes):
# `\` should be escaped as `\\`, but older versions of graphviz xdot
# output failed to properly escape it. See also
# https://github.com/jrfonseca/xdot.py/issues/92
if not broken_backslashes:
buf = re.sub(br'\\(.)', br'\1', buf)
self.parser = parser
self.buf = buf
self.pos = 0
self.pen = Pen()
self.shapes = []
def __bool__(self):
return self.pos < len(self.buf)
def read_code(self):
pos = self.buf.find(b" ", self.pos)
res = self.buf[self.pos:pos]
self.pos = pos + 1
self.skip_space()
res = res.decode('utf-8')
return res
def skip_space(self):
while self.pos < len(self.buf) and self.buf[self.pos:self.pos+1].isspace():
self.pos += 1
def read_int(self):
return int(self.read_code())
def read_float(self):
return float(self.read_code())
def read_point(self):
x = self.read_float()
y = self.read_float()
return self.transform(x, y)
def read_text(self):
num = self.read_int()
pos = self.buf.find(b"-", self.pos) + 1
self.pos = pos + num
res = self.buf[pos:self.pos]
self.skip_space()
res = res.decode('utf-8')
return res
def read_polygon(self):
n = self.read_int()
p = []
for i in range(n):
x, y = self.read_point()
p.append((x, y))
return p
def read_color(self):
# See http://www.graphviz.org/doc/info/attrs.html#k:color
c = self.read_text()
c1 = c[:1]
if c1 == '#':
hex2float = lambda h: float(int(h, 16)/255.0)
r = hex2float(c[1:3])
g = hex2float(c[3:5])
b = hex2float(c[5:7])
try:
a = hex2float(c[7:9])
except (IndexError, ValueError):
a = 1.0
return r, g, b, a
elif c1.isdigit() or c1 == ".":
# "H,S,V" or "H S V" or "H, S, V" or any other variation
h, s, v = map(float, c.replace(",", " ").split())
r, g, b = colorsys.hsv_to_rgb(h, s, v)
a = 1.0
return r, g, b, a
elif c1 == "[" or c1 == "(":
sys.stderr.write('warning: color gradients not supported yet\n')
return None
else:
return lookup_color(c)
def parse(self):
s = self
while s:
op = s.read_code()
if op == "c":
color = s.read_color()
if color is not None:
self.handle_color(color, filled=False)
elif op == "C":
color = s.read_color()
if color is not None:
self.handle_color(color, filled=True)
elif op == "S":
# http://www.graphviz.org/doc/info/attrs.html#k:style
style = s.read_text()
if style.startswith("setlinewidth("):
lw = style.split("(")[1].split(")")[0]
lw = float(lw)
self.handle_linewidth(lw)
elif style in ("solid", "dashed", "dotted"):
self.handle_linestyle(style)
elif op == "F":
size = s.read_float()
name = s.read_text()
self.handle_font(size, name)
elif op == "T":
x, y = s.read_point()
j = s.read_int()
w = s.read_float()
t = s.read_text()
self.handle_text(x, y, j, w, t)
elif op == "t":
f = s.read_int()
self.handle_font_characteristics(f)
elif op == "E":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=True)
elif op == "e":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=False)
elif op == "L":
points = self.read_polygon()
self.handle_line(points)
elif op == "B":
points = self.read_polygon()
self.handle_bezier(points, filled=False)
elif op == "b":
points = self.read_polygon()
self.handle_bezier(points, filled=True)
elif op == "P":
points = self.read_polygon()
self.handle_polygon(points, filled=True)
elif op == "p":
points = self.read_polygon()
self.handle_polygon(points, filled=False)
elif op == "I":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
path = s.read_text()
self.handle_image(x0, y0, w, h, path)
else:
sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
sys.exit(1)
return self.shapes
def transform(self, x, y):
return self.parser.transform(x, y)
def handle_color(self, color, filled=False):
if filled:
self.pen.fillcolor = color
else:
self.pen.color = color
def handle_linewidth(self, linewidth):
self.pen.linewidth = linewidth
def handle_linestyle(self, style):
if style == "solid":
self.pen.dash = ()
elif style == "dashed":
self.pen.dash = (6, ) # 6pt on, 6pt off
elif style == "dotted":
self.pen.dash = (2, 4) # 2pt on, 4pt off
def handle_font(self, size, name):
self.pen.fontsize = size
self.pen.fontname = name
def handle_font_characteristics(self, flags):
self.pen.bold = bool(flags & Pen.BOLD)
self.pen.italic = bool(flags & Pen.ITALIC)
self.pen.underline = bool(flags & Pen.UNDERLINE)
self.pen.superscript = bool(flags & Pen.SUPERSCRIPT)
self.pen.subscript = bool(flags & Pen.SUBSCRIPT)
self.pen.strikethrough = bool(flags & Pen.STRIKE_THROUGH)
self.pen.overline = bool(flags & Pen.OVERLINE)
if self.pen.overline:
sys.stderr.write('warning: overlined text not supported yet\n')
def handle_text(self, x, y, j, w, t):
self.shapes.append(elements.TextShape(self.pen, x, y, j, w, t))
def handle_ellipse(self, x0, y0, w, h, filled=False):
if filled:
# xdot uses this to mean "draw a filled shape with an outline"
self.shapes.append(elements.EllipseShape(self.pen, x0, y0, w, h, filled=True))
self.shapes.append(elements.EllipseShape(self.pen, x0, y0, w, h))
def handle_image(self, x0, y0, w, h, path):
self.shapes.append(elements.ImageShape(self.pen, x0, y0, w, h, path))
def handle_line(self, points):
self.shapes.append(elements.LineShape(self.pen, points))
def handle_bezier(self, points, filled=False):
if filled:
# xdot uses this to mean "draw a filled shape with an outline"
self.shapes.append(elements.BezierShape(self.pen, points, filled=True))
self.shapes.append(elements.BezierShape(self.pen, points))
def handle_polygon(self, points, filled=False):
if filled:
# xdot uses this to mean "draw a filled shape with an outline"
self.shapes.append(elements.PolygonShape(self.pen, points, filled=True))
self.shapes.append(elements.PolygonShape(self.pen, points))
class XDotParser(DotParser):
XDOTVERSION = '1.7'
def __init__(self, xdotcode, graphviz_version=None):
lexer = DotLexer(buf=xdotcode)
DotParser.__init__(self, lexer)
# https://github.com/jrfonseca/xdot.py/issues/92
self.broken_backslashes = False
if graphviz_version is not None and \
Version(graphviz_version) < Version("2.46.0"):
self.broken_backslashes = True
self.charset = 'utf-8'
self.nodes = []
self.edges = []
self.shapes = []
self.node_by_name = {}
self.top_graph = True
self.width = 0
self.height = 0
self.outputorder = 'breadthfirst'
def handle_graph(self, attrs):
if self.top_graph:
# Check xdot version
try:
xdotversion = attrs['xdotversion']
except KeyError:
pass
else:
if float(xdotversion) > float(self.XDOTVERSION):
sys.stderr.write('warning: xdot version %s, but supported is %s\n' %
(xdotversion, self.XDOTVERSION))
try:
self.charset = attrs['charset'].decode('ascii')
except KeyError:
pass
# Parse output order
try:
self.outputorder = attrs['outputorder'].decode(self.charset)
except KeyError:
pass
# Parse bounding box
try:
bb = attrs['bb']
except KeyError:
return
if bb:
xmin, ymin, xmax, ymax = map(float, bb.split(b","))
self.xoffset = -xmin
self.yoffset = -ymax
self.xscale = 1.0
self.yscale = -1.0
# FIXME: scale from points to pixels
self.width = max(xmax - xmin, 1)
self.height = max(ymax - ymin, 1)
self.top_graph = False
for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
if attr in attrs:
parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
self.shapes.extend(parser.parse())
def decode_attr(self, attrs, name):
try:
value = attrs[name]
except KeyError:
return None
else:
return value.decode(self.charset)
@staticmethod
def interpret_esc_nl(esc_string: Union[str, None]):
r"""Interpret newline escape sequences.
\n, \l and \r are replaced with newlines, other escaped
characters such as \\ with themselves.
"""
if esc_string is None:
return None
result = ""
was_escape = False
for ch in esc_string:
if was_escape:
was_escape = False
if ch in ['n', 'l', 'r']:
result += "\n"
else:
result += ch
else:
if ch == "\\":
was_escape = True
else:
result += ch
return result
def handle_node(self, id, attrs):
try:
pos = attrs['pos']
except KeyError:
# Node without pos attribute, most likely a subgraph. We need to
# create a Node object nevertheless, when one doesn't exist
# already, so that any edges to/from it don't get lost.
if id not in self.node_by_name:
# TODO: Extract the position from subgraph > graph > bb attribute.
node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None, None)
self.node_by_name[id] = node
return
x, y = self.parse_node_pos(pos)
w = float(attrs.get('width', 0))*72
h = float(attrs.get('height', 0))*72
shapes = []
for attr in ("_draw_", "_ldraw_"):
if attr in attrs:
parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
shapes.extend(parser.parse())
url = self.decode_attr(attrs, 'URL')
tooltip = self.interpret_esc_nl(self.decode_attr(attrs, 'tooltip'))
node = elements.Node(id, x, y, w, h, shapes, url, tooltip)
self.node_by_name[id] = node
if shapes:
self.nodes.append(node)
def handle_edge(self, src_id, dst_id, attrs):
try:
pos = attrs['pos']
except KeyError:
return
points = self.parse_edge_pos(pos)
shapes = []
for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
if attr in attrs:
parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
shapes.extend(parser.parse())
if shapes:
src = self.node_by_name[src_id]
dst = self.node_by_name[dst_id]
tooltip = self.interpret_esc_nl(self.decode_attr(attrs, 'tooltip'))
edge_url = (self.decode_attr(attrs, 'edgeURL') or self.decode_attr(attrs, 'edgehref') or
self.decode_attr(attrs, 'URL') or self.decode_attr(attrs, 'href'))
head_url = self.decode_attr(attrs, 'headURL') or self.decode_attr(attrs, 'headhref')
tail_url = self.decode_attr(attrs, 'tailURL') or self.decode_attr(attrs, 'tailhref')
url = {
'body': edge_url,
'head': head_url or edge_url,
'tail': tail_url or edge_url
}
self.edges.append(elements.Edge(src, dst, points, shapes, tooltip, url))
def parse(self):
DotParser.parse(self)
return elements.Graph(self.width, self.height, self.shapes,
self.nodes, self.edges, self.outputorder)
def parse_node_pos(self, pos):
x, y = pos.split(b",")
return self.transform(float(x), float(y))
def parse_edge_pos(self, pos):
points = []
for entry in pos.split(b' '):
fields = entry.split(b',')
try:
x, y = fields
except ValueError:
# TODO: handle start/end points
continue
else:
points.append(self.transform(float(x), float(y)))
return points
def transform(self, x, y):
# XXX: this is not the right place for this code
x = (x + self.xoffset)*self.xscale
y = (y + self.yoffset)*self.yscale
return x, y
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/actions.py 0000644 0001751 0001751 00000013257 15115636571 015433 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gdk, Gtk
from xdot.ui.elements import Jump
class DragAction(object):
def __init__(self, dot_widget):
self.dot_widget = dot_widget
def on_button_press(self, event):
self.startmousex = self.prevmousex = event.x
self.startmousey = self.prevmousey = event.y
self.start()
def on_motion_notify(self, event):
if event.is_hint:
window, x, y, state = event.window.get_device_position(event.device)
else:
x, y, state = event.x, event.y, event.state
deltax = self.prevmousex - x
deltay = self.prevmousey - y
self.drag(deltax, deltay)
self.prevmousex = x
self.prevmousey = y
def on_button_release(self, event):
self.stopmousex = event.x
self.stopmousey = event.y
self.stop()
def draw(self, cr):
pass
def start(self):
pass
def drag(self, deltax, deltay):
pass
def stop(self):
pass
def abort(self):
pass
class NullAction(DragAction):
# FIXME: The NullAction class is probably not the best place to hold this
# sort mutable global state.
_tooltip_window = Gtk.Window.new(type=Gtk.WindowType.POPUP)
_tooltip_label = Gtk.Label(xalign=0, yalign=0)
_tooltip_item = None
_tooltip_window.add(_tooltip_label)
_tooltip_label.show()
def on_motion_notify(self, event):
if event.is_hint:
window, x, y, state = event.window.get_device_position(event.device)
else:
x, y, state = event.x, event.y, event.state
dot_widget = self.dot_widget
item = dot_widget.get_url(x, y)
if item is None:
item = dot_widget.get_jump(x, y)
if item is not None:
NullAction._tooltip_window.set_transient_for(dot_widget.get_toplevel())
dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
dot_widget.set_highlight(item.highlight)
if item is not NullAction._tooltip_item:
# TODO: Should fold this into a method.
if isinstance(item, Jump) and item.item.tooltip is not None:
NullAction._tooltip_label.set_markup(item.item.tooltip)
NullAction._tooltip_window.resize(
NullAction._tooltip_label.get_preferred_width().natural_width,
NullAction._tooltip_label.get_preferred_height().natural_height
)
NullAction._tooltip_window.show()
else:
NullAction._tooltip_window.hide()
NullAction._tooltip_label.set_markup("")
NullAction._tooltip_item = item
if NullAction._tooltip_window.is_visible:
pointer = NullAction._tooltip_window.get_screen().get_root_window().get_pointer()
NullAction._tooltip_window.move(pointer.x + 15, pointer.y + 10)
else:
dot_widget.get_window().set_cursor(None)
dot_widget.set_highlight(None)
NullAction._tooltip_window.hide()
NullAction._tooltip_label.set_markup("")
NullAction._tooltip_item = None
class PanAction(DragAction):
def start(self):
self.dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.FLEUR))
def drag(self, deltax, deltay):
self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
self.dot_widget.queue_draw()
def stop(self):
self.dot_widget.get_window().set_cursor(None)
abort = stop
class ZoomAction(DragAction):
def drag(self, deltax, deltay):
self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
self.dot_widget.zoom_to_fit_on_resize = False
self.dot_widget.queue_draw()
def stop(self):
self.dot_widget.queue_draw()
class ZoomAreaAction(DragAction):
def drag(self, deltax, deltay):
self.dot_widget.queue_draw()
def draw(self, cr):
cr.save()
cr.set_source_rgba(.5, .5, 1.0, 0.25)
cr.rectangle(self.startmousex, self.startmousey,
self.prevmousex - self.startmousex,
self.prevmousey - self.startmousey)
cr.fill()
cr.set_source_rgba(.5, .5, 1.0, 1.0)
cr.set_line_width(1)
cr.rectangle(self.startmousex - .5, self.startmousey - .5,
self.prevmousex - self.startmousex + 1,
self.prevmousey - self.startmousey + 1)
cr.stroke()
cr.restore()
def stop(self):
x1, y1 = self.dot_widget.window2graph(self.startmousex,
self.startmousey)
x2, y2 = self.dot_widget.window2graph(self.stopmousex,
self.stopmousey)
self.dot_widget.zoom_to_area(x1, y1, x2, y2)
def abort(self):
self.dot_widget.queue_draw()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/animation.py 0000644 0001751 0001751 00000007005 15115636571 015744 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import math
import time
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import GLib
class Animation(object):
step = 0.03 # seconds
def __init__(self, dot_widget):
self.dot_widget = dot_widget
self.timeout_id = None
def start(self):
self.timeout_id = GLib.timeout_add(int(self.step * 1000), self.__real_tick)
def stop(self):
self.dot_widget.animation = NoAnimation(self.dot_widget)
if self.timeout_id is not None:
GLib.source_remove(self.timeout_id)
self.timeout_id = None
def __real_tick(self):
try:
if not self.tick():
self.stop()
return False
except AttributeError as e:
self.stop()
raise e
return True
def tick(self):
return False
class NoAnimation(Animation):
def start(self):
pass
def stop(self):
pass
class LinearAnimation(Animation):
duration = 0.6
def start(self):
self.started = time.time()
Animation.start(self)
def tick(self):
t = (time.time() - self.started) / self.duration
self.animate(max(0, min(t, 1)))
return (t < 1)
def animate(self, t):
pass
class MoveToAnimation(LinearAnimation):
def __init__(self, dot_widget, target_x, target_y):
Animation.__init__(self, dot_widget)
self.source_x = dot_widget.x
self.source_y = dot_widget.y
self.target_x = target_x
self.target_y = target_y
def animate(self, t):
sx, sy = self.source_x, self.source_y
tx, ty = self.target_x, self.target_y
self.dot_widget.x = tx * t + sx * (1 - t)
self.dot_widget.y = ty * t + sy * (1 - t)
self.dot_widget.queue_draw()
class ZoomToAnimation(MoveToAnimation):
def __init__(self, dot_widget, target_x, target_y):
MoveToAnimation.__init__(self, dot_widget, target_x, target_y)
self.source_zoom = dot_widget.zoom_ratio
self.target_zoom = self.source_zoom
self.extra_zoom = 0
middle_zoom = 0.5 * (self.source_zoom + self.target_zoom)
distance = math.hypot(self.source_x - self.target_x,
self.source_y - self.target_y)
rect = self.dot_widget.get_allocation()
visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio
visible *= 0.9
if distance > 0:
desired_middle_zoom = visible / distance
self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
def animate(self, t):
a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom
self.dot_widget.zoom_ratio = c*t + b*t*(1 - t) + a*(1 - t)
self.dot_widget.zoom_to_fit_on_resize = False
MoveToAnimation.animate(self, t)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/colors.py 0000644 0001751 0001751 00000103307 15115636571 015270 0 ustar 00runner runner # Apache-Style Software License for ColorBrewer software and ColorBrewer Color
# Schemes, Version 1.1
#
# Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State
# University. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions as source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. The end-user documentation included with the redistribution, if any,
# must include the following acknowledgment:
#
# This product includes color specifications and designs developed by
# Cynthia Brewer (http://colorbrewer.org/).
#
# Alternately, this acknowledgment may appear in the software itself, if and
# wherever such third-party acknowledgments normally appear.
#
# 3. The name "ColorBrewer" must not be used to endorse or promote products
# derived from this software without prior written permission. For written
# permission, please contact Cynthia Brewer at cbrewer@psu.edu.
#
# 4. Products derived from this software may not be called "ColorBrewer",
# nor may "ColorBrewer" appear in their name, without prior written
# permission of Cynthia Brewer.
#
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA
# BREWER, MARK HARROWER, OR THE PENNSYLVANIA STATE UNIVERSITY BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
brewer_colors = {
'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)],
'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],
'accent5': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176)],
'accent6': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127)],
'accent7': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23)],
'accent8': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23), (102, 102, 102)],
'blues3': [(222, 235, 247), (158, 202, 225), (49, 130, 189)],
'blues4': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (33, 113, 181)],
'blues5': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
'blues6': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
'blues7': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
'blues8': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
'blues9': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 81, 156), (8, 48, 107)],
'brbg10': [(84, 48, 5), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
'brbg11': [(84, 48, 5), (1, 102, 94), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143)],
'brbg3': [(216, 179, 101), (245, 245, 245), (90, 180, 172)],
'brbg4': [(166, 97, 26), (223, 194, 125), (128, 205, 193), (1, 133, 113)],
'brbg5': [(166, 97, 26), (223, 194, 125), (245, 245, 245), (128, 205, 193), (1, 133, 113)],
'brbg6': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
'brbg7': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (245, 245, 245), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
'brbg8': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
'brbg9': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
'bugn3': [(229, 245, 249), (153, 216, 201), (44, 162, 95)],
'bugn4': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (35, 139, 69)],
'bugn5': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
'bugn6': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
'bugn7': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
'bugn8': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
'bugn9': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
'bupu3': [(224, 236, 244), (158, 188, 218), (136, 86, 167)],
'bupu4': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 65, 157)],
'bupu5': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
'bupu6': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
'bupu7': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
'bupu8': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
'bupu9': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (129, 15, 124), (77, 0, 75)],
'dark23': [(27, 158, 119), (217, 95, 2), (117, 112, 179)],
'dark24': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138)],
'dark25': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30)],
'dark26': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2)],
'dark27': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29)],
'dark28': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29), (102, 102, 102)],
'gnbu3': [(224, 243, 219), (168, 221, 181), (67, 162, 202)],
'gnbu4': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (43, 140, 190)],
'gnbu5': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
'gnbu6': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
'gnbu7': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
'gnbu8': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
'gnbu9': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 104, 172), (8, 64, 129)],
'greens3': [(229, 245, 224), (161, 217, 155), (49, 163, 84)],
'greens4': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (35, 139, 69)],
'greens5': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
'greens6': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
'greens7': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
'greens8': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
'greens9': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
'greys3': [(240, 240, 240), (189, 189, 189), (99, 99, 99)],
'greys4': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (82, 82, 82)],
'greys5': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
'greys6': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
'greys7': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
'greys8': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
'greys9': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37), (0, 0, 0)],
'oranges3': [(254, 230, 206), (253, 174, 107), (230, 85, 13)],
'oranges4': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (217, 71, 1)],
'oranges5': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
'oranges6': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
'oranges7': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
'oranges8': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
'oranges9': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (166, 54, 3), (127, 39, 4)],
'orrd3': [(254, 232, 200), (253, 187, 132), (227, 74, 51)],
'orrd4': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (215, 48, 31)],
'orrd5': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
'orrd6': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
'orrd7': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
'orrd8': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
'orrd9': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (179, 0, 0), (127, 0, 0)],
'paired10': [(166, 206, 227), (106, 61, 154), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
'paired11': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
'paired12': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (177, 89, 40), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
'paired3': [(166, 206, 227), (31, 120, 180), (178, 223, 138)],
'paired4': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44)],
'paired5': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153)],
'paired6': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28)],
'paired7': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111)],
'paired8': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0)],
'paired9': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
'pastel13': [(251, 180, 174), (179, 205, 227), (204, 235, 197)],
'pastel14': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228)],
'pastel15': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166)],
'pastel16': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204)],
'pastel17': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189)],
'pastel18': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236)],
'pastel19': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236), (242, 242, 242)],
'pastel23': [(179, 226, 205), (253, 205, 172), (203, 213, 232)],
'pastel24': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228)],
'pastel25': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201)],
'pastel26': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174)],
'pastel27': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204)],
'pastel28': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204), (204, 204, 204)],
'piyg10': [(142, 1, 82), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
'piyg11': [(142, 1, 82), (77, 146, 33), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65)],
'piyg3': [(233, 163, 201), (247, 247, 247), (161, 215, 106)],
'piyg4': [(208, 28, 139), (241, 182, 218), (184, 225, 134), (77, 172, 38)],
'piyg5': [(208, 28, 139), (241, 182, 218), (247, 247, 247), (184, 225, 134), (77, 172, 38)],
'piyg6': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
'piyg7': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (247, 247, 247), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
'piyg8': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
'piyg9': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
'prgn10': [(64, 0, 75), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
'prgn11': [(64, 0, 75), (27, 120, 55), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97)],
'prgn3': [(175, 141, 195), (247, 247, 247), (127, 191, 123)],
'prgn4': [(123, 50, 148), (194, 165, 207), (166, 219, 160), (0, 136, 55)],
'prgn5': [(123, 50, 148), (194, 165, 207), (247, 247, 247), (166, 219, 160), (0, 136, 55)],
'prgn6': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
'prgn7': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (247, 247, 247), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
'prgn8': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
'prgn9': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
'pubu3': [(236, 231, 242), (166, 189, 219), (43, 140, 190)],
'pubu4': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (5, 112, 176)],
'pubu5': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
'pubu6': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
'pubu7': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
'pubu8': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
'pubu9': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (4, 90, 141), (2, 56, 88)],
'pubugn3': [(236, 226, 240), (166, 189, 219), (28, 144, 153)],
'pubugn4': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (2, 129, 138)],
'pubugn5': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
'pubugn6': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
'pubugn7': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
'pubugn8': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
'pubugn9': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 108, 89), (1, 70, 54)],
'puor10': [(127, 59, 8), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
'puor11': [(127, 59, 8), (84, 39, 136), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172)],
'puor3': [(241, 163, 64), (247, 247, 247), (153, 142, 195)],
'puor4': [(230, 97, 1), (253, 184, 99), (178, 171, 210), (94, 60, 153)],
'puor5': [(230, 97, 1), (253, 184, 99), (247, 247, 247), (178, 171, 210), (94, 60, 153)],
'puor6': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
'puor7': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (247, 247, 247), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
'puor8': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
'puor9': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
'purd3': [(231, 225, 239), (201, 148, 199), (221, 28, 119)],
'purd4': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (206, 18, 86)],
'purd5': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
'purd6': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
'purd7': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
'purd8': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
'purd9': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (152, 0, 67), (103, 0, 31)],
'purples3': [(239, 237, 245), (188, 189, 220), (117, 107, 177)],
'purples4': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (106, 81, 163)],
'purples5': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
'purples6': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
'purples7': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
'purples8': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
'purples9': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (84, 39, 143), (63, 0, 125)],
'rdbu10': [(103, 0, 31), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
'rdbu11': [(103, 0, 31), (33, 102, 172), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195)],
'rdbu3': [(239, 138, 98), (247, 247, 247), (103, 169, 207)],
'rdbu4': [(202, 0, 32), (244, 165, 130), (146, 197, 222), (5, 113, 176)],
'rdbu5': [(202, 0, 32), (244, 165, 130), (247, 247, 247), (146, 197, 222), (5, 113, 176)],
'rdbu6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
'rdbu7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (247, 247, 247), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
'rdbu8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
'rdbu9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
'rdgy10': [(103, 0, 31), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
'rdgy11': [(103, 0, 31), (77, 77, 77), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135)],
'rdgy3': [(239, 138, 98), (255, 255, 255), (153, 153, 153)],
'rdgy4': [(202, 0, 32), (244, 165, 130), (186, 186, 186), (64, 64, 64)],
'rdgy5': [(202, 0, 32), (244, 165, 130), (255, 255, 255), (186, 186, 186), (64, 64, 64)],
'rdgy6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
'rdgy7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (255, 255, 255), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
'rdgy8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
'rdgy9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
'rdpu3': [(253, 224, 221), (250, 159, 181), (197, 27, 138)],
'rdpu4': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (174, 1, 126)],
'rdpu5': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
'rdpu6': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
'rdpu7': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
'rdpu8': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
'rdpu9': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119), (73, 0, 106)],
'rdylbu10': [(165, 0, 38), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
'rdylbu11': [(165, 0, 38), (69, 117, 180), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209)],
'rdylbu3': [(252, 141, 89), (255, 255, 191), (145, 191, 219)],
'rdylbu4': [(215, 25, 28), (253, 174, 97), (171, 217, 233), (44, 123, 182)],
'rdylbu5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 217, 233), (44, 123, 182)],
'rdylbu6': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
'rdylbu7': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (255, 255, 191), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
'rdylbu8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
'rdylbu9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
'rdylgn10': [(165, 0, 38), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
'rdylgn11': [(165, 0, 38), (26, 152, 80), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99)],
'rdylgn3': [(252, 141, 89), (255, 255, 191), (145, 207, 96)],
'rdylgn4': [(215, 25, 28), (253, 174, 97), (166, 217, 106), (26, 150, 65)],
'rdylgn5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (166, 217, 106), (26, 150, 65)],
'rdylgn6': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
'rdylgn7': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (255, 255, 191), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
'rdylgn8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
'rdylgn9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
'reds3': [(254, 224, 210), (252, 146, 114), (222, 45, 38)],
'reds4': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (203, 24, 29)],
'reds5': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
'reds6': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
'reds7': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
'reds8': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
'reds9': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (165, 15, 21), (103, 0, 13)],
'set13': [(228, 26, 28), (55, 126, 184), (77, 175, 74)],
'set14': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163)],
'set15': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0)],
'set16': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51)],
'set17': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40)],
'set18': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191)],
'set19': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191), (153, 153, 153)],
'set23': [(102, 194, 165), (252, 141, 98), (141, 160, 203)],
'set24': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195)],
'set25': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84)],
'set26': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47)],
'set27': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148)],
'set28': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148), (179, 179, 179)],
'set310': [(141, 211, 199), (188, 128, 189), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
'set311': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
'set312': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 237, 111), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
'set33': [(141, 211, 199), (255, 255, 179), (190, 186, 218)],
'set34': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114)],
'set35': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211)],
'set36': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98)],
'set37': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105)],
'set38': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229)],
'set39': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
'spectral10': [(158, 1, 66), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
'spectral11': [(158, 1, 66), (50, 136, 189), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165)],
'spectral3': [(252, 141, 89), (255, 255, 191), (153, 213, 148)],
'spectral4': [(215, 25, 28), (253, 174, 97), (171, 221, 164), (43, 131, 186)],
'spectral5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 221, 164), (43, 131, 186)],
'spectral6': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
'spectral7': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (255, 255, 191), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
'spectral8': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
'spectral9': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
'ylgn3': [(247, 252, 185), (173, 221, 142), (49, 163, 84)],
'ylgn4': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (35, 132, 67)],
'ylgn5': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
'ylgn6': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
'ylgn7': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
'ylgn8': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
'ylgn9': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 104, 55), (0, 69, 41)],
'ylgnbu3': [(237, 248, 177), (127, 205, 187), (44, 127, 184)],
'ylgnbu4': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (34, 94, 168)],
'ylgnbu5': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
'ylgnbu6': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
'ylgnbu7': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
'ylgnbu8': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
'ylgnbu9': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (37, 52, 148), (8, 29, 88)],
'ylorbr3': [(255, 247, 188), (254, 196, 79), (217, 95, 14)],
'ylorbr4': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (204, 76, 2)],
'ylorbr5': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
'ylorbr6': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
'ylorbr7': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
'ylorbr8': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
'ylorbr9': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (153, 52, 4), (102, 37, 6)],
'ylorrd3': [(255, 237, 160), (254, 178, 76), (240, 59, 32)],
'ylorrd4': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (227, 26, 28)],
'ylorrd5': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
'ylorrd6': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
'ylorrd7': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
'ylorrd8': [(255, 255, 204), (255, 237, 160), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
}
def lookup_color(c):
"""Return RGBA values of color c
c should be either an X11 color or a brewer color set and index
e.g. "navajowhite", "greens3/2"
"""
import sys
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gdk
try:
color = Gdk.color_parse(c)
except ValueError:
pass
else:
s = 1.0/65535.0
r = color.red*s
g = color.green*s
b = color.blue*s
a = 1.0
return r, g, b, a
try:
dummy, scheme, index = c.split('/')
r, g, b = brewer_colors[scheme][int(index)]
except (ValueError, KeyError):
pass
else:
s = 1.0/255.0
r = r*s
g = g*s
b = b*s
a = 1.0
return r, g, b, a
sys.stderr.write("warning: unknown color '%s'\n" % c)
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/elements.py 0000644 0001751 0001751 00000062527 15115636571 015613 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import math
import operator
import warnings
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import Pango
from gi.repository import PangoCairo
import cairo
import numpy
_inf = float('inf')
_get_bounding = operator.attrgetter('bounding')
class Shape:
"""Abstract base class for all the drawing shapes."""
bounding = (-_inf, -_inf, _inf, _inf)
def __init__(self):
pass
def _intersects(self, bounding):
x0, y0, x1, y1 = bounding
x2, y2, x3, y3 = self.bounding
return x2 <= x1 and x0 <= x3 and y2 <= y1 and y0 <= y3
def _fully_in(self, bounding):
x0, y0, x1, y1 = bounding
x2, y2, x3, y3 = self.bounding
return x0 <= x2 and x3 <= x1 and y0 <= y2 and y3 <= y1
def _draw(self, cr, highlight, bounding):
"""Actual draw implementation"""
raise NotImplementedError
def draw(self, cr, highlight=False, bounding=None):
"""Draw this shape with the given cairo context"""
if bounding is None or self._intersects(bounding):
self._draw(cr, highlight, bounding)
def select_pen(self, highlight):
if highlight:
if not hasattr(self, 'highlight_pen'):
self.highlight_pen = self.pen.highlighted()
return self.highlight_pen
else:
return self.pen
def search_text(self, regexp):
return False
def get_smallest_distance(self, x, y):
return None
@staticmethod
def _bounds_from_points(points):
x0, y0 = points[0]
x1, y1 = x0, y0
for i in range(1, len(points)):
x, y = points[i]
x0, x1 = min(x0, x), max(x1, x)
y0, y1 = min(y0, y), max(y1, y)
return x0, y0, x1, y1
@staticmethod
def _envelope_bounds(*args):
xa = ya = _inf
xb = yb = -_inf
for bounds in args:
for x0, y0, x1, y1 in bounds:
xa, xb = min(xa, x0), max(xb, x1)
ya, yb = min(ya, y0), max(yb, y1)
return xa, ya, xb, yb
def get_text(self):
return None
# Map PostScript fontnames to Pango description strings
# See also:
# - https://graphviz.org/docs/attrs/fontname/
# - https://gitlab.com/graphviz/graphviz/-/blob/main/lib/common/ps_font_equiv.h
# - https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html
# XXX: Ideally we'd call FontConfig to do this for us, but there seems to be no
# easy way to do that
fontname_map = {
# PostScript Level 1
'Courier': 'Courier',
'Courier-Bold': 'Courier Bold',
'Courier-Oblique': 'Courier Oblique',
'Courier-BoldOblique': 'Courier Bold Oblique',
'Helvetica': 'Helvetica',
'Helvetica-Bold': 'Helvetica Bold',
'Helvetica-Oblique': 'Helvetica Oblique',
'Helvetica-BoldOblique': 'Helvetica Bold Oblique',
'Times-Roman': 'Times Roman',
'Times-Bold': 'Times Bold',
'Times-Italic': 'Times Italic',
'Times-BoldItalic': 'Times Bold Italic',
'Symbol': 'Symbol',
# PostScript Level 2
'AvantGarde-Book': 'AvantGarde Book',
'AvantGarde-BookOblique': 'AvantGarde Book Oblique',
'AvantGarde-Demi': 'AvantGarde DemiBold',
'AvantGarde-DemiOblique': 'AvantGarde DemiBold Oblique',
'Bookman-Demi': 'Bookman DemiBold',
'Bookman-DemiItalic': 'Bookman DemiBold Italic',
'Bookman-Light': 'Bookman Light',
'Bookman-LightItalic': 'Bookman Light Italic',
'Helvetica-Narrow': 'Helvetica Condensed',
'Helvetica-Narrow-Bold': 'Helvetica Condensed Bold',
'Helvetica-Narrow-Oblique': 'Helvetica Condensed Oblique',
'Helvetica-Narrow-BoldOblique': 'Helvetica Condensed Bold Oblique',
'NewCenturySchlbk-Bold': 'NewCenturySchlbk Bold',
'NewCenturySchlbk-BoldItalic': 'NewCenturySchlbk Bold Italic',
'NewCenturySchlbk-Italic': 'NewCenturySchlbk Italic',
'NewCenturySchlbk-Roman': 'NewCenturySchlbk Roman',
'Palatino-Bold': 'Palatino Bold',
'Palatino-BoldItalic': 'Palatino Bold Italic',
'Palatino-Italic': 'Palatino Italic',
'Palatino-Roman': 'Palatino Roman',
'ZapfChancery-MediumItalic': 'ZapfChancery Medium Italic',
'ZapfDingbats': 'ZapfDingbats',
}
class TextShape(Shape):
LEFT, CENTER, RIGHT = -1, 0, 1
def __init__(self, pen, x, y, j, w, t):
Shape.__init__(self)
self.pen = pen.copy()
self.x = x
self.y = y
self.j = j # Centering
self.w = w # width
self.t = t # text
def _draw(self, cr, highlight, bounding):
try:
layout = self.layout
except AttributeError:
layout = PangoCairo.create_layout(cr)
# set font options
# see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
context = layout.get_context()
fo = cairo.FontOptions()
fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
fo.set_hint_style(cairo.HINT_STYLE_NONE)
fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
try:
PangoCairo.context_set_font_options(context, fo)
except TypeError:
# XXX: Some broken pangocairo bindings show the error
# 'TypeError: font_options must be a cairo.FontOptions or None'
pass
except KeyError:
# cairo.FontOptions is not registered as a foreign
# struct in older PyGObject versions.
# https://git.gnome.org/browse/pygobject/commit/?id=b21f66d2a399b8c9a36a1758107b7bdff0ec8eaa
pass
# https://developer.gnome.org/pango/stable/PangoMarkupFormat.html
markup = GObject.markup_escape_text(self.t)
if self.pen.bold:
markup = '' + markup + ''
if self.pen.italic:
markup = '' + markup + ''
if self.pen.underline:
markup = '' + markup + ''
if self.pen.strikethrough:
markup = '' + markup + ''
if self.pen.superscript:
markup = '' + markup + ''
if self.pen.subscript:
markup = '' + markup + ''
success, attrs, text, accel_char = Pango.parse_markup(markup, -1, '\x00')
assert success
layout.set_attributes(attrs)
# set font
fontname = fontname_map.get(self.pen.fontname, self.pen.fontname)
font = Pango.FontDescription(fontname)
font.set_absolute_size(self.pen.fontsize*Pango.SCALE)
layout.set_font_description(font)
# set text
layout.set_text(text, -1)
# cache it
self.layout = layout
else:
PangoCairo.update_layout(cr, layout)
descent = 2 # XXX get descender from font metrics
width, height = layout.get_size()
width = float(width)/Pango.SCALE
height = float(height)/Pango.SCALE
# we know the width that dot thinks this text should have
# we do not necessarily have a font with the same metrics
# scale it so that the text fits inside its box
if width > self.w:
f = self.w / width
width = self.w # equivalent to width *= f
height *= f
descent *= f
else:
f = 1.0
y = self.y - height + descent
if bounding is None or (y <= bounding[3] and bounding[1] <= y + height):
x = self.x - 0.5 * (1 + self.j) * width
cr.move_to(x, y)
cr.save()
cr.scale(f, f)
cr.set_source_rgba(*self.select_pen(highlight).color)
PangoCairo.show_layout(cr, layout)
cr.restore()
if 0: # DEBUG
# show where dot thinks the text should appear
cr.set_source_rgba(1, 0, 0, .9)
x = self.x - 0.5 * (1 + self.j) * width
cr.move_to(x, self.y)
cr.line_to(x+self.w, self.y)
cr.stroke()
def search_text(self, regexp):
return regexp.search(self.t) is not None
@property
def bounding(self):
x, w, j = self.x, self.w, self.j
return x - 0.5 * (1 + j) * w, -_inf, x + 0.5 * (1 - j) * w, _inf
def get_text(self):
return self.t
class ImageShape(Shape):
def __init__(self, pen, x0, y0, w, h, path):
Shape.__init__(self)
self.pen = pen.copy()
self.x0 = x0
self.y0 = y0
self.w = w
self.h = h
self.path = path
def _draw(self, cr, highlight, bounding):
pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path)
sx = float(self.w)/float(pixbuf.get_width())
sy = float(self.h)/float(pixbuf.get_height())
cr.save()
cr.translate(self.x0, self.y0 - self.h)
cr.scale(sx, sy)
Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
cr.paint()
cr.restore()
@property
def bounding(self):
x0, y0 = self.x0, self.y0
return x0, y0 - self.h, x0 + self.w, y0
class EllipseShape(Shape):
def __init__(self, pen, x0, y0, w, h, filled=False):
Shape.__init__(self)
self.pen = pen.copy()
self.x0 = x0
self.y0 = y0
self.w = w
self.h = h
self.filled = filled
def _draw(self, cr, highlight, bounding):
cr.save()
cr.translate(self.x0, self.y0)
cr.scale(self.w, self.h)
cr.move_to(1.0, 0.0)
cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
cr.restore()
pen = self.select_pen(highlight)
if self.filled:
cr.set_source_rgba(*pen.fillcolor)
cr.fill()
else:
cr.set_dash(pen.dash)
cr.set_line_width(pen.linewidth)
cr.set_source_rgba(*pen.color)
cr.stroke()
@property
def bounding(self):
x0, y0, w, h = self.x0, self.y0, self.w, self.h
bt = 0 if self.filled else self.pen.linewidth / 2.
w, h = w + bt, h + bt
return x0 - w, y0 - h, x0 + w, y0 + h
class PolygonShape(Shape):
def __init__(self, pen, points, filled=False):
Shape.__init__(self)
self.pen = pen.copy()
self.points = points
self.filled = filled
x0, y0, x1, y1 = Shape._bounds_from_points(self.points)
bt = 0 if self.filled else self.pen.linewidth / 2.
self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt
def _draw(self, cr, highlight, bounding):
x0, y0 = self.points[-1]
cr.move_to(x0, y0)
for x, y in self.points:
cr.line_to(x, y)
cr.close_path()
pen = self.select_pen(highlight)
if self.filled:
cr.set_source_rgba(*pen.fillcolor)
cr.fill_preserve()
cr.fill()
else:
cr.set_dash(pen.dash)
cr.set_line_width(pen.linewidth)
cr.set_source_rgba(*pen.color)
cr.stroke()
class LineShape(Shape):
def __init__(self, pen, points):
Shape.__init__(self)
self.pen = pen.copy()
self.points = points
x0, y0, x1, y1 = Shape._bounds_from_points(self.points)
bt = self.pen.linewidth / 2.
self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt
def _draw(self, cr, highlight, bounding):
x0, y0 = self.points[0]
cr.move_to(x0, y0)
for x1, y1 in self.points[1:]:
cr.line_to(x1, y1)
pen = self.select_pen(highlight)
cr.set_dash(pen.dash)
cr.set_line_width(pen.linewidth)
cr.set_source_rgba(*pen.color)
cr.stroke()
class BezierShape(Shape):
def __init__(self, pen, points, filled=False):
Shape.__init__(self)
self.pen = pen.copy()
self.points = points
self.filled = filled
x0, y0 = self.points[0]
xa = xb = x0
ya = yb = y0
for i in range(1, len(self.points), 3):
(x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3]
for t in self._cubic_bernstein_extrema(x0, x1, x2, x3):
if 0 < t < 1: # We're dealing only with Bezier curves
v = self._cubic_bernstein(x0, x1, x2, x3, t)
xa, xb = min(xa, v), max(xb, v)
xa, xb = min(xa, x3), max(xb, x3) # t=0 / t=1
for t in self._cubic_bernstein_extrema(y0, y1, y2, y3):
if 0 < t < 1: # We're dealing only with Bezier curves
v = self._cubic_bernstein(y0, y1, y2, y3, t)
ya, yb = min(ya, v), max(yb, v)
ya, yb = min(ya, y3), max(yb, y3) # t=0 / t=1
x0, y0 = x3, y3
bt = 0 if self.filled else self.pen.linewidth / 2.
self.bounding = xa - bt, ya - bt, xb + bt, yb + bt
@staticmethod
def _cubic_bernstein_extrema(p0, p1, p2, p3):
"""
Find extremas of a function of real domain defined by evaluating
a cubic bernstein polynomial of given bernstein coefficients.
"""
# compute coefficients of derivative
a = 3.*(p3-p0+3.*(p1-p2))
b = 6.*(p0+p2-2.*p1)
c = 3.*(p1-p0)
if a == 0:
if b == 0:
return () # constant
return (-c / b,) # linear
# quadratic
# compute discriminant
d = b*b - 4.*a*c
if d < 0:
return ()
k = -2. * a
if d == 0:
return (b / k,)
r = math.sqrt(d)
return ((b + r) / k, (b - r) / k)
@staticmethod
def _cubic_bernstein(p0, p1, p2, p3, t):
"""
Evaluate polynomial of given bernstein coefficients
using de Casteljau's algorithm.
"""
u = 1 - t
return p0*(u**3) + 3*t*u*(p1*u + p2*t) + p3*(t**3)
def _draw(self, cr, highlight, bounding):
x0, y0 = self.points[0]
cr.move_to(x0, y0)
for i in range(1, len(self.points), 3):
(x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3]
cr.curve_to(x1, y1, x2, y2, x3, y3)
pen = self.select_pen(highlight)
if self.filled:
cr.set_source_rgba(*pen.fillcolor)
cr.fill_preserve()
cr.fill()
else:
cr.set_dash(pen.dash)
cr.set_line_width(pen.linewidth)
cr.set_source_rgba(*pen.color)
cr.stroke()
def get_smallest_distance(self, x, y):
min_squared_distance = float('inf')
points_iter = iter(self.points)
x0, y0 = next(points_iter)
while True:
try:
x1, y1 = next(points_iter)
except StopIteration:
break
x2, y2 = next(points_iter)
x3, y3 = next(points_iter)
_e1 = -5
_e2 = (x0 - 3 * x1 + 3 * x2 - x3)
_e3 = (y0 - 3 * y1 + 3 * y2 - y3)
_e4 = 2 * x1
_e5 = 2 * y1
_e6 = x0**2
_e7 = y0**2
_e8 = -2
_e9 = (x0 - _e4 + x2)
_e10 = (y0 - _e5 + y2)
_e11 = 2 * x0
_e12 = 2 * y0
_e13 = 5 * _e6
_e14 = 5 * _e7
_e15 = x1**2
_e16 = y1**2
coefficients = [
(x - x0) * (x0 - x1) + (y - y0) * (y0 - y1),
_e13 + 3 * _e15 + _e11 * (_e1 * x1 + x2) - 2 * x * _e9 + _e14 + 3 * _e16 + _e12 * (_e1 * y1 + y2) - 2 * y * _e10,
-10 * _e6 + 9 * x1 * (_e8 * x1 + x2) + x * _e2 + x0 * (30 * x1 - 12 * x2 + x3) - 10 * _e7 + 9 * y1 * (_e8 * y1 + y2) + y * _e3 + y0 * (30 * y1 - 12 * y2 + y3),
2 * (_e13 + 18 * _e15 + 3 * x2**2 + _e4 * (-9 * x2 + x3) - _e11 * (10 * x1 - 6 * x2 + x3) + _e14 + 3 * (6 * _e16 - 6 * y1 * y2 + y2**2) + _e5 * y3 - _e12 * (10 * y1 - 6 * y2 + y3)),
_e1 * _e9 * _e2 - 5 * _e10 * _e3,
_e2**2 + _e3**2
]
coefficients.reverse()
for t in numpy.roots(coefficients):
if 1e-6 < abs(t.imag):
continue
t = t.real
if t < 0:
t = 0
elif 1 < t:
t = 1
squared_distance = ((1 - t)**3 * x0 + 3 * (1 - t)**2 * t * x1 + 3 * (1 - t) * t**2 * x2 + t**3 * x3 - x)**2 + ((1 - t)**3 * y0 + 3 * (1 - t)**2 * t * y1 + 3 * (1 - t) * t**2 * y2 + t**3 * y3 - y)**2
if squared_distance < min_squared_distance:
min_squared_distance = squared_distance
x0 = x3
y0 = y3
return math.sqrt(min_squared_distance)
class CompoundShape(Shape):
def __init__(self, shapes):
Shape.__init__(self)
self.shapes = shapes
self.bounding = Shape._envelope_bounds(map(_get_bounding, self.shapes))
def _draw(self, cr, highlight, bounding):
if bounding is not None and self._fully_in(bounding):
bounding = None
for shape in self.shapes:
if bounding is None or shape._intersects(bounding):
shape._draw(cr, highlight, bounding)
def search_text(self, regexp):
for shape in self.shapes:
if shape.search_text(regexp):
return True
return False
def get_text(self):
for shape in self.shapes:
text = shape.get_text()
if text is not None:
return text
return None
class Url(object):
def __init__(self, item, url, highlight=None):
self.item = item
self.url = url
if highlight is None:
highlight = set([item])
self.highlight = highlight
class Jump(object):
def __init__(self, item, x, y, highlight=None):
self.item = item
self.x = x
self.y = y
if highlight is None:
highlight = set([item])
self.highlight = highlight
class Element(CompoundShape):
"""Base class for graph nodes and edges."""
def __init__(self, shapes):
CompoundShape.__init__(self, shapes)
def is_inside(self, x, y):
return False
def get_url(self, x, y):
return None
def get_jump(self, x, y):
return None
class Node(Element):
def __init__(self, id, x, y, w, h, shapes, url, tooltip):
Element.__init__(self, shapes)
self.id = id
self.x = x
self.y = y
self.x1 = x - 0.5*w
self.y1 = y - 0.5*h
self.x2 = x + 0.5*w
self.y2 = y + 0.5*h
self.url = url
self.tooltip = tooltip
def is_inside(self, x, y):
return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
def get_url(self, x, y):
if self.url is None:
return None
if self.is_inside(x, y):
return Url(self, self.url)
return None
def get_jump(self, x, y):
if self.is_inside(x, y):
return Jump(self, self.x, self.y)
return None
def __repr__(self):
return "" % self.id
def square_distance(x1, y1, x2, y2):
deltax = x2 - x1
deltay = y2 - y1
return deltax*deltax + deltay*deltay
class Edge(Element):
def __init__(self, src, dst, points, shapes, tooltip, url):
Element.__init__(self, shapes)
self.src = src
self.dst = dst
self.points = points
self.tooltip = tooltip
self.url = url
RADIUS = 10
def is_inside_begin(self, x, y):
return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
def is_inside_end(self, x, y):
return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
def is_inside(self, x, y):
if self.is_inside_begin(x, y):
return True
if self.is_inside_end(x, y):
return True
for shape in self.shapes:
min_dist = shape.get_smallest_distance(x, y)
if min_dist is not None and min_dist <= self.RADIUS:
return True
return False
def get_jump(self, x, y, to_dst = False):
for shape in self.shapes:
x1, y1, x2, y2 = shape.bounding
if (x1 - self.RADIUS) <= x and x <= (x2 + self.RADIUS) and (y1 - self.RADIUS) <= y and y <= (y2 + self.RADIUS):
break
else:
return None
for shape in self.shapes:
distance = shape.get_smallest_distance(x, y)
if distance is not None and distance <= self.RADIUS:
jmp_dest = self.dst if to_dst else self.src
return Jump(self, jmp_dest.x, jmp_dest.y)
return None
def get_url(self, x, y):
if self.is_inside_begin(x, y) and self.url['head'] is not None:
return Url(self, self.url['head'])
if self.is_inside_end(x, y) and self.url['tail'] is not None:
return Url(self, self.url['tail'])
if self.is_inside(x, y) and self.url['body'] is not None:
return Url(self, self.url['body'])
return None
def __repr__(self):
return " %s>" % (self.src, self.dst)
class Graph(Shape):
def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=(), outputorder='breadthfirst'):
Shape.__init__(self)
self.width = width
self.height = height
self.shapes = shapes
self.nodes = nodes
self.edges = edges
self.outputorder = outputorder
self.bounding = Shape._envelope_bounds(
map(_get_bounding, self.shapes),
map(_get_bounding, self.nodes),
map(_get_bounding, self.edges))
def get_size(self):
return self.width, self.height
def _draw_shapes(self, cr, bounding, highlight_items):
for shape in self.shapes:
if bounding is None or shape._intersects(bounding):
shape._draw(cr, highlight=(shape in highlight_items), bounding=bounding)
def _draw_nodes(self, cr, bounding, highlight_items):
highlight_nodes = []
for element in highlight_items:
if isinstance(element, Edge):
highlight_nodes.append(element.src)
highlight_nodes.append(element.dst)
else:
highlight_nodes.append(element)
for node in self.nodes:
if bounding is None or node._intersects(bounding):
node._draw(cr, highlight=(node in highlight_nodes), bounding=bounding)
def _draw_edges(self, cr, bounding, highlight_items):
for edge in self.edges:
if bounding is None or edge._intersects(bounding):
should_highlight = any(e in highlight_items
for e in (edge, edge.src, edge.dst))
edge._draw(cr, highlight=should_highlight, bounding=bounding)
def draw(self, cr, highlight_items=None, bounding=None):
if bounding is not None:
if not self._intersects(bounding):
return
if self._fully_in(bounding):
bounding = None
if highlight_items is None:
highlight_items = ()
cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
cr.set_line_cap(cairo.LINE_CAP_BUTT)
cr.set_line_join(cairo.LINE_JOIN_MITER)
self._draw_shapes(cr, bounding, highlight_items)
if self.outputorder == 'edgesfirst':
self._draw_edges(cr, bounding, highlight_items)
self._draw_nodes(cr, bounding, highlight_items)
else:
self._draw_nodes(cr, bounding, highlight_items)
self._draw_edges(cr, bounding, highlight_items)
def get_element(self, x, y):
for node in self.nodes:
if node.is_inside(x, y):
return node
for edge in self.edges:
if edge.is_inside(x, y):
return edge
return None
def get_url(self, x, y):
for node in self.nodes:
url = node.get_url(x, y)
if url is not None:
return url
for edge in self.edges:
url = edge.get_url(x, y)
if url is not None:
return url
return None
def get_jump(self, x, y, to_dst = False):
for edge in self.edges:
jump = edge.get_jump(x, y, to_dst)
if jump is not None:
return jump
for node in self.nodes:
jump = node.get_jump(x, y)
if jump is not None:
return jump
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/pen.py 0000644 0001751 0001751 00000003145 15115636571 014550 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
class Pen:
"""Store pen attributes."""
BOLD = 1
ITALIC = 2
UNDERLINE = 4
SUPERSCRIPT = 8
SUBSCRIPT = 16
STRIKE_THROUGH = 32
OVERLINE = 64
def __init__(self):
# set default attributes
self.color = (0.0, 0.0, 0.0, 1.0)
self.fillcolor = (0.0, 0.0, 0.0, 1.0)
self.linewidth = 1.0
self.fontsize = 14.0
self.fontname = "Times-Roman"
self.bold = False
self.italic = False
self.underline = False
self.superscript = False
self.subscript = False
self.strikethrough = False
self.overline = False
self.dash = ()
def copy(self):
"""Create a copy of this pen."""
pen = Pen()
pen.__dict__ = self.__dict__.copy()
return pen
def highlighted(self):
pen = self.copy()
pen.color = (1, 0, 0, 1)
pen.fillcolor = (1, .8, .8, 1)
return pen
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227897.0
xdot-1.6/xdot/ui/window.py 0000644 0001751 0001751 00000074037 15115636571 015305 0 ustar 00runner runner # Copyright 2008-2015 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
import math
import os
import re
import subprocess
import sys
import time
import operator
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
# For pygtk inspiration and guidance see:
# - http://mirageiv.berlios.de/
# - http://comix.sourceforge.net/
from . import actions
from ..dot.lexer import ParseError
from ._xdotparser import XDotParser
from . import animation
from . import actions
from .elements import Graph
class DotWidget(Gtk.DrawingArea):
"""GTK widget that draws dot graphs."""
# TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object.
__gsignals__ = {
'clicked': (GObject.SignalFlags.RUN_LAST, None, (str, object)),
'error': (GObject.SignalFlags.RUN_LAST, None, (str,)),
'history': (GObject.SignalFlags.RUN_LAST, None, (bool, bool))
}
filter = 'dot'
graphviz_version = None
def __init__(self):
Gtk.DrawingArea.__init__(self)
self.graph = Graph()
self.openfilename = None
self.set_can_focus(True)
self.connect("draw", self.on_draw)
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK)
self.connect("button-press-event", self.on_area_button_press)
self.connect("button-release-event", self.on_area_button_release)
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK |
Gdk.EventMask.POINTER_MOTION_HINT_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.SCROLL_MASK |
Gdk.EventMask.SMOOTH_SCROLL_MASK)
self.connect("motion-notify-event", self.on_area_motion_notify)
self.connect("scroll-event", self.on_area_scroll_event)
self.connect("size-allocate", self.on_area_size_allocate)
self.connect('key-press-event', self.on_key_press_event)
self.last_mtime = None
self.mtime_changed = False
GLib.timeout_add(1000, self.update)
self.x, self.y = 0.0, 0.0
self.zoom_ratio = 1.0
self.zoom_to_fit_on_resize = False
self.animation = animation.NoAnimation(self)
self.drag_action = actions.NullAction(self)
self.presstime = None
self.highlight = None
self.highlight_search = False
self.history_back = []
self.history_forward = []
self.zoom_gesture = Gtk.GestureZoom.new(self)
self.zoom_gesture.connect("scale-changed", self.on_scale_changed)
def error_dialog(self, message):
self.emit('error', message)
def set_filter(self, filter):
self.filter = filter
self.graphviz_version = None
def run_filter(self, dotcode):
if not self.filter:
return dotcode
try:
p = subprocess.Popen(
[self.filter, '-Txdot'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
universal_newlines=False
)
except OSError as exc:
error = '%s: %s' % (self.filter, exc.strerror)
p = subprocess.CalledProcessError(exc.errno, self.filter, exc.strerror)
else:
xdotcode, error = p.communicate(dotcode)
error = error.decode()
error = error.rstrip()
if error:
sys.stderr.write(error + '\n')
if p.returncode != 0:
self.error_dialog(error)
return xdotcode
def _set_dotcode(self, dotcode, filename=None, center=True):
# By default DOT language is UTF-8, but it accepts other encodings
assert isinstance(dotcode, bytes)
xdotcode = self.run_filter(dotcode)
if xdotcode is None:
return False
try:
self.set_xdotcode(xdotcode, center=center)
except ParseError as ex:
self.error_dialog(str(ex))
return False
else:
return True
def set_dotcode(self, dotcode, filename=None, center=True):
self.openfilename = None
if self._set_dotcode(dotcode, filename, center=center):
if filename is None:
self.last_mtime = None
else:
self.last_mtime = os.stat(filename).st_mtime
self.mtime_changed = False
self.openfilename = filename
return True
def set_xdotcode(self, xdotcode, center=True):
assert isinstance(xdotcode, bytes)
if self.graphviz_version is None and self.filter is not None:
stdout = subprocess.check_output([self.filter, '-V'], stderr=subprocess.STDOUT)
stdout = stdout.rstrip()
mo = re.match(br'^.* - .* version (?P.*) \(.*\)$', stdout)
assert mo
self.graphviz_version = mo.group('version').decode('ascii')
parser = XDotParser(xdotcode, graphviz_version=self.graphviz_version)
self.graph = parser.parse()
self.zoom_image(self.zoom_ratio, center=center)
def reload(self):
if self.openfilename is not None:
try:
fp = open(self.openfilename, 'rb')
self._set_dotcode(fp.read(), self.openfilename, center=False)
fp.close()
except IOError:
pass
else:
del self.history_back[:], self.history_forward[:]
def update(self):
if self.openfilename is not None:
try:
current_mtime = os.stat(self.openfilename).st_mtime
except OSError:
return True
if current_mtime != self.last_mtime:
self.last_mtime = current_mtime
self.mtime_changed = True
elif self.mtime_changed:
self.mtime_changed = False
self.reload()
return True
def _draw_graph(self, cr, rect):
w, h = float(rect.width), float(rect.height)
cx, cy = 0.5 * w, 0.5 * h
x, y, ratio = self.x, self.y, self.zoom_ratio
x0, y0 = x - cx / ratio, y - cy / ratio
x1, y1 = x0 + w / ratio, y0 + h / ratio
bounding = (x0, y0, x1, y1)
cr.translate(cx, cy)
cr.scale(ratio, ratio)
cr.translate(-x, -y)
self.graph.draw(cr, highlight_items=self.highlight, bounding=bounding)
def on_draw(self, widget, cr):
rect = self.get_allocation()
Gtk.render_background(self.get_style_context(), cr, 0, 0,
rect.width, rect.height)
cr.save()
self._draw_graph(cr, rect)
cr.restore()
self.drag_action.draw(cr)
return False
def get_current_pos(self):
return self.x, self.y
def set_current_pos(self, x, y):
self.x = x
self.y = y
self.queue_draw()
def set_highlight(self, items, search=False):
# Enable or disable search highlight
if search:
self.highlight_search = items is not None
# Ignore cursor highlight while searching
if self.highlight_search and not search:
return
if self.highlight != items:
self.highlight = items
self.queue_draw()
def zoom_image(self, zoom_ratio, center=False, pos=None):
# Constrain zoom ratio to a sane range to prevent numeric instability.
zoom_ratio = min(zoom_ratio, 1E4)
zoom_ratio = max(zoom_ratio, 1E-6)
if center:
self.x = self.graph.width/2
self.y = self.graph.height/2
elif pos is not None:
rect = self.get_allocation()
x, y = pos
x -= 0.5*rect.width
y -= 0.5*rect.height
self.x += x / self.zoom_ratio - x / zoom_ratio
self.y += y / self.zoom_ratio - y / zoom_ratio
self.zoom_ratio = zoom_ratio
self.zoom_to_fit_on_resize = False
self.queue_draw()
def zoom_to_area(self, x1, y1, x2, y2):
rect = self.get_allocation()
width = abs(x1 - x2)
height = abs(y1 - y2)
if width == 0 and height == 0:
self.zoom_ratio *= self.ZOOM_INCREMENT
else:
self.zoom_ratio = min(
float(rect.width)/float(width),
float(rect.height)/float(height)
)
self.zoom_to_fit_on_resize = False
self.x = (x1 + x2) / 2
self.y = (y1 + y2) / 2
self.queue_draw()
def zoom_to_fit(self):
rect = self.get_allocation()
rect.x += self.ZOOM_TO_FIT_MARGIN
rect.y += self.ZOOM_TO_FIT_MARGIN
rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN
rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN
zoom_ratio = min(
float(rect.width)/float(self.graph.width),
float(rect.height)/float(self.graph.height)
)
self.zoom_image(zoom_ratio, center=True)
self.zoom_to_fit_on_resize = True
ZOOM_INCREMENT = 1.25
ZOOM_TO_FIT_MARGIN = 12
def on_zoom_in(self, action):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
def on_zoom_out(self, action):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
def on_zoom_fit(self, action):
self.zoom_to_fit()
def on_zoom_100(self, action):
self.zoom_image(1.0)
POS_INCREMENT = 100
def on_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Left:
self.x -= self.POS_INCREMENT/self.zoom_ratio
self.queue_draw()
return True
if event.keyval == Gdk.KEY_Right:
self.x += self.POS_INCREMENT/self.zoom_ratio
self.queue_draw()
return True
if event.keyval == Gdk.KEY_Up:
self.y -= self.POS_INCREMENT/self.zoom_ratio
self.queue_draw()
return True
if event.keyval == Gdk.KEY_Down:
self.y += self.POS_INCREMENT/self.zoom_ratio
self.queue_draw()
return True
if event.keyval in (Gdk.KEY_Page_Up,
Gdk.KEY_plus,
Gdk.KEY_equal,
Gdk.KEY_KP_Add):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
self.queue_draw()
return True
if event.keyval in (Gdk.KEY_Page_Down,
Gdk.KEY_minus,
Gdk.KEY_KP_Subtract):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
self.queue_draw()
return True
if event.keyval == Gdk.KEY_Escape:
self.drag_action.abort()
self.drag_action = actions.NullAction(self)
return True
if event.keyval == Gdk.KEY_r:
self.reload()
return True
if event.keyval == Gdk.KEY_f:
win = widget.get_toplevel()
find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
textentry = find_toolitem.get_children()
win.set_focus(textentry[0])
return True
if event.keyval == Gdk.KEY_q:
Gtk.main_quit()
return True
if event.keyval == Gdk.KEY_p:
self.on_print()
return True
if event.keyval == Gdk.KEY_t:
# toggle toolbar visibility
win = widget.get_toplevel()
toolbar = win.uimanager.get_widget("/ToolBar")
toolbar.set_visible(not toolbar.get_visible())
return True
if event.keyval == Gdk.KEY_w:
self.zoom_to_fit()
return True
return False
print_settings = None
def on_print(self, action=None):
print_op = Gtk.PrintOperation()
if self.print_settings is not None:
print_op.set_print_settings(self.print_settings)
print_op.connect("begin_print", self.begin_print)
print_op.connect("draw_page", self.draw_page)
res = print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.get_toplevel())
if res == Gtk.PrintOperationResult.APPLY:
self.print_settings = print_op.get_print_settings()
def begin_print(self, operation, context):
operation.set_n_pages(1)
return True
def draw_page(self, operation, context, page_nr):
cr = context.get_cairo_context()
rect = self.get_allocation()
self._draw_graph(cr, rect)
def get_drag_action(self, event):
state = event.state
if event.button in (1, 2): # left or middle button
modifiers = Gtk.accelerator_get_default_mod_mask()
if state & modifiers == Gdk.ModifierType.CONTROL_MASK:
return actions.ZoomAction
elif state & modifiers == Gdk.ModifierType.SHIFT_MASK:
return actions.ZoomAreaAction
else:
return actions.PanAction
return actions.NullAction
def on_area_button_press(self, area, event):
self.animation.stop()
self.drag_action.abort()
action_type = self.get_drag_action(event)
self.drag_action = action_type(self)
self.drag_action.on_button_press(event)
self.presstime = time.time()
self.pressx = event.x
self.pressy = event.y
return False
def is_click(self, event, click_fuzz=4, click_timeout=1.0):
assert event.type == Gdk.EventType.BUTTON_RELEASE
if self.presstime is None:
# got a button release without seeing the press?
return False
# XXX instead of doing this complicated logic, shouldn't we listen
# for gtk's clicked event instead?
deltax = self.pressx - event.x
deltay = self.pressy - event.y
return (time.time() < self.presstime + click_timeout and
math.hypot(deltax, deltay) < click_fuzz)
def on_click(self, element, event):
"""Override this method in subclass to process
click events. Note that element can be None
(click on empty space)."""
return False
def on_area_button_release(self, area, event):
self.drag_action.on_button_release(event)
self.drag_action = actions.NullAction(self)
x, y = int(event.x), int(event.y)
if self.is_click(event):
el = self.get_element(x, y)
if self.on_click(el, event):
return True
if event.button == 1:
url = self.get_url(x, y)
if url is not None:
self.emit('clicked', url.url, event)
else:
ctrl_held = event.state & Gdk.ModifierType.CONTROL_MASK
jump = self.get_jump(x, y, to_dst=ctrl_held)
if jump is not None:
self.animate_to(jump.x, jump.y)
return True
if event.button == 1 or event.button == 2:
return True
return False
def on_area_scroll_event(self, area, event):
if event.direction == Gdk.ScrollDirection.UP:
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
pos=(event.x, event.y))
return True
elif event.direction == Gdk.ScrollDirection.DOWN:
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
pos=(event.x, event.y))
else:
deltas = event.get_scroll_deltas()
self.zoom_image(self.zoom_ratio * (1 - deltas.delta_y / 10),
pos=(event.x, event.y))
return True
return False
def on_area_motion_notify(self, area, event):
self.drag_action.on_motion_notify(event)
return True
def on_area_size_allocate(self, area, allocation):
if self.zoom_to_fit_on_resize:
self.zoom_to_fit()
def on_scale_changed(self, gesture, scale):
point, x, y = gesture.get_point()
if point:
pos = (x, y)
new_zoom_ratio = self.zoom_ratio * math.exp(math.log(scale) / 3)
self.zoom_image(new_zoom_ratio, pos=pos)
def animate_to(self, x, y):
del self.history_forward[:]
self.history_back.append(self.get_current_pos())
self.history_changed()
self._animate_to(x, y)
def _animate_to(self, x, y):
self.animation = animation.ZoomToAnimation(self, x, y)
self.animation.start()
def history_changed(self):
self.emit(
'history',
bool(self.history_back),
bool(self.history_forward))
def on_go_back(self, action=None):
try:
item = self.history_back.pop()
except LookupError:
return
self.history_forward.append(self.get_current_pos())
self.history_changed()
self._animate_to(*item)
def on_go_forward(self, action=None):
try:
item = self.history_forward.pop()
except LookupError:
return
self.history_back.append(self.get_current_pos())
self.history_changed()
self._animate_to(*item)
def window2graph(self, x, y):
rect = self.get_allocation()
x -= 0.5*rect.width
y -= 0.5*rect.height
x /= self.zoom_ratio
y /= self.zoom_ratio
x += self.x
y += self.y
return x, y
def get_element(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_element(x, y)
def get_url(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_url(x, y)
def get_jump(self, x, y, to_dst = False):
x, y = self.window2graph(x, y)
return self.graph.get_jump(x, y, to_dst)
class FindMenuToolAction(Gtk.Action):
__gtype_name__ = "FindMenuToolAction"
def do_create_tool_item(self):
return Gtk.ToolItem()
class DotWindow(Gtk.Window):
ui = '''
'''
base_title = 'Dot Viewer'
def __init__(self, widget=None, width=512, height=512):
Gtk.Window.__init__(self)
self.graph = Graph()
window = self
window.set_title(self.base_title)
window.set_default_size(width, height)
window.set_wmclass("xdot", "xdot")
vbox = Gtk.VBox()
window.add(vbox)
self.dotwidget = widget or DotWidget()
self.dotwidget.connect("error", lambda e, m: self.error_dialog(m))
self.dotwidget.connect("history", self.on_history)
# Create a UIManager instance
uimanager = self.uimanager = Gtk.UIManager()
# Add the accelerator group to the toplevel window
accelgroup = uimanager.get_accel_group()
window.add_accel_group(accelgroup)
# Create an ActionGroup
actiongroup = Gtk.ActionGroup('Actions')
self.actiongroup = actiongroup
# Create actions
actiongroup.add_actions((
('Open', Gtk.STOCK_OPEN, None, None, "Open dot-file", self.on_open),
('Export', Gtk.STOCK_SAVE_AS, None, None, "Export graph to other format", self.on_export),
('Reload', Gtk.STOCK_REFRESH, None, None, "Reload graph", self.on_reload),
('Print', Gtk.STOCK_PRINT, None, None,
"Prints the currently visible part of the graph", self.dotwidget.on_print),
('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, "Zoom in", self.dotwidget.on_zoom_in),
('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, "Zoom out", self.dotwidget.on_zoom_out),
('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, "Fit zoom", self.dotwidget.on_zoom_fit),
('Zoom100', Gtk.STOCK_ZOOM_100, None, None, "Reset zoom level", self.dotwidget.on_zoom_100),
('FindNext', Gtk.STOCK_GO_FORWARD, 'Next Result', None, 'Move to the next search result', self.on_find_next),
))
self.back_action = Gtk.Action('Back', None, None, Gtk.STOCK_GO_BACK)
self.back_action.set_sensitive(False)
self.back_action.connect("activate", self.dotwidget.on_go_back)
actiongroup.add_action(self.back_action)
self.forward_action = Gtk.Action('Forward', None, None, Gtk.STOCK_GO_FORWARD)
self.forward_action.set_sensitive(False)
self.forward_action.connect("activate", self.dotwidget.on_go_forward)
actiongroup.add_action(self.forward_action)
find_action = FindMenuToolAction("Find", None,
"Find a node by name", None)
actiongroup.add_action(find_action)
findstatus_action = FindMenuToolAction("FindStatus", None,
"Number of results found", None)
actiongroup.add_action(findstatus_action)
# Add the actiongroup to the uimanager
uimanager.insert_action_group(actiongroup, 0)
# Add a UI descrption
uimanager.add_ui_from_string(self.ui)
# Create a Toolbar
toolbar = uimanager.get_widget('/ToolBar')
vbox.pack_start(toolbar, False, False, 0)
vbox.pack_start(self.dotwidget, True, True, 0)
self.last_open_dir = "."
self.set_focus(self.dotwidget)
# Add Find text search
find_toolitem = uimanager.get_widget('/ToolBar/Find')
self.textentry = Gtk.Entry()
self.textentry.set_icon_from_stock(0, Gtk.STOCK_FIND)
find_toolitem.add(self.textentry)
self.textentry.set_activates_default(True)
self.textentry.connect("activate", self.textentry_activate, self.textentry);
self.textentry.connect("changed", self.textentry_changed, self.textentry);
uimanager.get_widget('/ToolBar/FindNextSeparator').set_draw(False)
uimanager.get_widget('/ToolBar/FindStatusSeparator').set_draw(False)
self.find_next_toolitem = uimanager.get_widget('/ToolBar/FindNext')
self.find_next_toolitem.set_sensitive(False)
self.find_count = Gtk.Label()
findstatus_toolitem = uimanager.get_widget('/ToolBar/FindStatus')
findstatus_toolitem.add(self.find_count)
self.show_all()
def find_text(self, entry_text):
found_items = []
dot_widget = self.dotwidget
try:
regexp = re.compile(entry_text)
except re.error as err:
sys.stderr.write('warning: re.compile() failed with error "%s"\n' % err)
return []
for element in dot_widget.graph.nodes + dot_widget.graph.edges + dot_widget.graph.shapes:
if element.search_text(regexp):
found_items.append(element)
return sorted(found_items, key=operator.methodcaller('get_text'))
def textentry_changed(self, widget, entry):
self.find_count.set_label('')
self.find_index = 0
self.find_next_toolitem.set_sensitive(False)
entry_text = entry.get_text()
dot_widget = self.dotwidget
if not entry_text:
dot_widget.set_highlight(None, search=True)
return
found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items, search=True)
if found_items:
self.find_count.set_label('%d nodes found' % len(found_items))
def textentry_activate(self, widget, entry):
self.find_index = 0
self.find_next_toolitem.set_sensitive(False)
entry_text = entry.get_text()
dot_widget = self.dotwidget
if not entry_text:
dot_widget.set_highlight(None, search=True)
self.set_focus(self.dotwidget)
return
found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items, search=True)
if found_items:
dot_widget.animate_to(found_items[0].x, found_items[0].y)
self.find_next_toolitem.set_sensitive(len(found_items) > 1)
def set_filter(self, filter):
self.dotwidget.set_filter(filter)
def set_dotcode(self, dotcode, filename=None):
if self.dotwidget.set_dotcode(dotcode, filename):
self.update_title(filename)
self.dotwidget.zoom_to_fit()
def set_xdotcode(self, xdotcode, filename=None):
if self.dotwidget.set_xdotcode(xdotcode):
self.update_title(filename)
self.dotwidget.zoom_to_fit()
def update_title(self, filename=None):
if filename is None:
self.set_title(self.base_title)
else:
self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
def open_file(self, filename):
try:
fp = open(filename, 'rb')
self.set_dotcode(fp.read(), filename)
fp.close()
except IOError as ex:
self.error_dialog(str(ex))
def on_open(self, action):
chooser = Gtk.FileChooserDialog(parent=self,
title="Open Graphviz File",
action=Gtk.FileChooserAction.OPEN,
buttons=(Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN,
Gtk.ResponseType.OK))
chooser.set_default_response(Gtk.ResponseType.OK)
chooser.set_current_folder(self.last_open_dir)
filter = Gtk.FileFilter()
filter.set_name("Graphviz files")
filter.add_pattern("*.gv")
filter.add_pattern("*.dot")
chooser.add_filter(filter)
filter = Gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
chooser.add_filter(filter)
if chooser.run() == Gtk.ResponseType.OK:
filename = chooser.get_filename()
self.last_open_dir = chooser.get_current_folder()
chooser.destroy()
self.open_file(filename)
else:
chooser.destroy()
def export_file(self, filename, format_):
if not filename.endswith("." + format_):
filename += '.' + format_
cmd = [
self.dotwidget.filter, # program name, usually "dot"
'-T' + format_,
'-o', filename,
self.dotwidget.openfilename,
]
subprocess.check_call(cmd)
def on_export(self, action):
if self.dotwidget.openfilename is None:
return
default_filter = "PNG image"
output_formats = {
"dot file": "dot",
"GIF image": "gif",
"JPG image": "jpg",
"JSON": "json",
"PDF": "pdf",
"PNG image": "png",
"PostScript": "ps",
"SVG image": "svg",
"XFIG image": "fig",
"xdot file": "xdot",
}
buttons = (
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
chooser = Gtk.FileChooserDialog(
parent=self,
title="Export to other file format.",
action=Gtk.FileChooserAction.SAVE,
buttons=buttons)
chooser.set_default_response(Gtk.ResponseType.OK)
chooser.set_current_folder(self.last_open_dir)
openfilename = os.path.basename(self.dotwidget.openfilename)
openfileroot = os.path.splitext(openfilename)[0]
chooser.set_current_name(openfileroot)
for name, ext in output_formats.items():
filter_ = Gtk.FileFilter()
filter_.set_name(name)
filter_.add_pattern('*.' + ext)
chooser.add_filter(filter_)
if name == default_filter:
chooser.set_filter(filter_)
if chooser.run() == Gtk.ResponseType.OK:
filename = chooser.get_filename()
format_ = output_formats[chooser.get_filter().get_name()]
chooser.destroy()
self.export_file(filename, format_)
else:
chooser.destroy()
def on_reload(self, action):
self.dotwidget.reload()
def error_dialog(self, message):
dlg = Gtk.MessageDialog(parent=self,
type=Gtk.MessageType.ERROR,
message_format=message,
buttons=Gtk.ButtonsType.OK)
dlg.set_title(self.base_title)
dlg.run()
dlg.destroy()
def on_find_next(self, action):
self.find_index += 1
entry_text = self.textentry.get_text()
# Maybe storing the search result would be better
found_items = self.find_text(entry_text)
found_item = found_items[self.find_index]
self.dotwidget.animate_to(found_item.x, found_item.y)
self.find_next_toolitem.set_sensitive(len(found_items) > self.find_index + 1)
def on_history(self, action, has_back, has_forward):
self.back_action.set_sensitive(has_back)
self.forward_action.set_sensitive(has_forward)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1765227930.9790883
xdot-1.6/xdot.egg-info/ 0000755 0001751 0001751 00000000000 15115636633 014465 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/PKG-INFO 0000644 0001751 0001751 00000002260 15115636632 015561 0 ustar 00runner runner Metadata-Version: 2.1
Name: xdot
Version: 1.6
Summary: Interactive viewer for Graphviz dot files
Home-page: https://github.com/jrfonseca/xdot.py
Author: Jose Fonseca
Author-email: jose.r.fonseca@gmail.com
License: LGPL
Platform: UNKNOWN
Classifier: Development Status :: 6 - Mature
Classifier: Environment :: X11 Applications :: GTK
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: OS Independent
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Multimedia :: Graphics :: Viewers
License-File: LICENSE.txt
xdot.py is an interactive viewer for graphs written in Graphviz's dot
language.
It uses internally the graphviz's xdot output format as an intermediate
format, and PyGTK and Cairo for rendering.
xdot.py can be used either as a standalone application from command
line, or as a library embedded in your python application.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/SOURCES.txt 0000644 0001751 0001751 00000000722 15115636632 016351 0 ustar 00runner runner LICENSE.txt
README.md
setup.py
xdot/__init__.py
xdot/__main__.py
xdot.egg-info/PKG-INFO
xdot.egg-info/SOURCES.txt
xdot.egg-info/dependency_links.txt
xdot.egg-info/entry_points.txt
xdot.egg-info/requires.txt
xdot.egg-info/top_level.txt
xdot/dot/__init__.py
xdot/dot/lexer.py
xdot/dot/parser.py
xdot/dot/scanner.py
xdot/ui/__init__.py
xdot/ui/_xdotparser.py
xdot/ui/actions.py
xdot/ui/animation.py
xdot/ui/colors.py
xdot/ui/elements.py
xdot/ui/pen.py
xdot/ui/window.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/dependency_links.txt 0000644 0001751 0001751 00000000001 15115636632 020532 0 ustar 00runner runner
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/entry_points.txt 0000644 0001751 0001751 00000000051 15115636632 017756 0 ustar 00runner runner [gui_scripts]
xdot = xdot.__main__:main
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/requires.txt 0000644 0001751 0001751 00000000032 15115636632 017057 0 ustar 00runner runner PyGObject
numpy
packaging
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1765227930.0
xdot-1.6/xdot.egg-info/top_level.txt 0000644 0001751 0001751 00000000026 15115636632 017214 0 ustar 00runner runner xdot
xdot/dot
xdot/ui