summaryrefslogtreecommitdiffstats
path: root/zenmap/radialnet/gui
diff options
context:
space:
mode:
Diffstat (limited to 'zenmap/radialnet/gui')
-rw-r--r--zenmap/radialnet/gui/Application.py159
-rw-r--r--zenmap/radialnet/gui/ControlWidget.py1270
-rw-r--r--zenmap/radialnet/gui/Dialogs.py89
-rw-r--r--zenmap/radialnet/gui/HostsViewer.py231
-rw-r--r--zenmap/radialnet/gui/Image.py164
-rw-r--r--zenmap/radialnet/gui/LegendWindow.py242
-rw-r--r--zenmap/radialnet/gui/NodeNotebook.py742
-rw-r--r--zenmap/radialnet/gui/NodeWindow.py129
-rw-r--r--zenmap/radialnet/gui/RadialNet.py2020
-rw-r--r--zenmap/radialnet/gui/SaveDialog.py169
-rw-r--r--zenmap/radialnet/gui/Toolbar.py309
-rw-r--r--zenmap/radialnet/gui/__init__.py56
12 files changed, 5580 insertions, 0 deletions
diff --git a/zenmap/radialnet/gui/Application.py b/zenmap/radialnet/gui/Application.py
new file mode 100644
index 0000000..dbc6fe2
--- /dev/null
+++ b/zenmap/radialnet/gui/Application.py
@@ -0,0 +1,159 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+from radialnet.util.integration import make_graph_from_nmap_parser
+from radialnet.core.Info import INFO
+from radialnet.core.XMLHandler import XMLReader
+from radialnet.gui.ControlWidget import ControlWidget, ControlFisheye
+from radialnet.gui.Toolbar import Toolbar
+from radialnet.gui.Image import Pixmaps
+import radialnet.gui.RadialNet as RadialNet
+from radialnet.bestwidgets.windows import BWMainWindow, BWAlertDialog
+from radialnet.bestwidgets.boxes import BWHBox, BWVBox, BWStatusbar
+
+
+DIMENSION = (640, 480)
+
+
+class Application(BWMainWindow):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ BWMainWindow.__init__(self)
+ self.set_default_size(DIMENSION[0], DIMENSION[1])
+
+ self.set_icon(Pixmaps().get_pixbuf('logo'))
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__hbox = BWHBox(spacing=0)
+ self.__vbox = BWVBox(spacing=0)
+
+ self.__radialnet = RadialNet.RadialNet(RadialNet.LAYOUT_WEIGHTED)
+ self.__control = ControlWidget(self.__radialnet)
+ self.__fisheye = ControlFisheye(self.__radialnet)
+ self.__toolbar = Toolbar(self.__radialnet,
+ self,
+ self.__control,
+ self.__fisheye)
+ self.__statusbar = BWStatusbar()
+
+ self.__hbox.bw_pack_start_expand_fill(self.__radialnet)
+ self.__hbox.bw_pack_start_noexpand_nofill(self.__control)
+
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__toolbar)
+ self.__vbox.bw_pack_start_expand_fill(self.__hbox)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__fisheye)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__statusbar)
+
+ self.add(self.__vbox)
+ self.set_title(" ".join([INFO['name'], INFO['version']]))
+ self.set_position(Gtk.WindowPosition.CENTER)
+ self.show_all()
+ self.connect('destroy', Gtk.main_quit)
+
+ self.__radialnet.set_no_show_all(True)
+ self.__control.set_no_show_all(True)
+ self.__fisheye.set_no_show_all(True)
+
+ self.__radialnet.hide()
+ self.__control.hide()
+ self.__fisheye.hide()
+ self.__toolbar.disable_controls()
+
+ def parse_nmap_xml_file(self, file):
+ """
+ """
+ try:
+
+ self.__parser = XMLReader(file)
+ self.__parser.parse()
+
+ except Exception as e:
+
+ text = 'It is not possible open file %s: %s' % (file, e)
+
+ alert = BWAlertDialog(self,
+ primary_text='Error opening file.',
+ secondary_text=text)
+
+ alert.show_all()
+
+ return False
+
+ self.__radialnet.set_empty()
+ self.__radialnet.set_graph(make_graph_from_nmap_parser(self.__parser))
+ self.__radialnet.show()
+
+ self.__toolbar.enable_controls()
+
+ return True
+
+ def start(self):
+ """
+ """
+ Gtk.main()
diff --git a/zenmap/radialnet/gui/ControlWidget.py b/zenmap/radialnet/gui/ControlWidget.py
new file mode 100644
index 0000000..99be773
--- /dev/null
+++ b/zenmap/radialnet/gui/ControlWidget.py
@@ -0,0 +1,1270 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Gdk
+
+import math
+
+import radialnet.util.geometry as geometry
+
+from radialnet.bestwidgets.boxes import *
+from radialnet.core.Coordinate import PolarCoordinate
+import radialnet.gui.RadialNet as RadialNet
+from radialnet.bestwidgets.expanders import BWExpander
+
+
+OPTIONS = ['address',
+ 'hostname',
+ 'icon',
+ 'latency',
+ 'ring',
+ 'region',
+ 'slow in/out']
+
+REFRESH_RATE = 500
+
+
+class ControlWidget(BWVBox):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWVBox.__init__(self)
+ self.set_border_width(6)
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__action = ControlAction(self.radialnet)
+ self.__interpolation = ControlInterpolation(self.radialnet)
+ self.__layout = ControlLayout(self.radialnet)
+ self.__view = ControlView(self.radialnet)
+
+ self.bw_pack_start_noexpand_nofill(self.__action)
+ self.bw_pack_start_noexpand_nofill(self.__interpolation)
+ self.bw_pack_start_noexpand_nofill(self.__layout)
+ self.bw_pack_start_noexpand_nofill(self.__view)
+
+
+class ControlAction(BWExpander):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWExpander.__init__(self, _('Action'))
+ self.set_expanded(True)
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__tbox = BWTable(1, 4)
+ self.__tbox.bw_set_spacing(0)
+ self.__vbox = BWVBox()
+
+ self.__jump_to = Gtk.RadioToolButton(group=None,
+ stock_id=Gtk.STOCK_JUMP_TO)
+ self.__jump_to.set_tooltip_text('Change focus')
+ self.__jump_to.connect('toggled',
+ self.__change_pointer,
+ RadialNet.POINTER_JUMP_TO)
+
+ self.__info = Gtk.RadioToolButton(group=self.__jump_to,
+ stock_id=Gtk.STOCK_INFO)
+ self.__info.set_tooltip_text('Show information')
+ self.__info.connect('toggled',
+ self.__change_pointer,
+ RadialNet.POINTER_INFO)
+
+ self.__group = Gtk.RadioToolButton(group=self.__jump_to,
+ stock_id=Gtk.STOCK_ADD)
+ self.__group.set_tooltip_text('Group children')
+ self.__group.connect('toggled',
+ self.__change_pointer,
+ RadialNet.POINTER_GROUP)
+
+ self.__region = Gtk.RadioToolButton(group=self.__jump_to,
+ stock_id=Gtk.STOCK_SELECT_COLOR)
+ self.__region.set_tooltip_text('Fill region')
+ self.__region.connect('toggled',
+ self.__change_pointer,
+ RadialNet.POINTER_FILL)
+
+ self.__region_color = Gtk.ComboBoxText.new()
+ self.__region_color.append_text(_('Red'))
+ self.__region_color.append_text(_('Yellow'))
+ self.__region_color.append_text(_('Green'))
+ self.__region_color.connect('changed', self.__change_region)
+ self.__region_color.set_active(self.radialnet.get_region_color())
+
+ self.__tbox.bw_attach_next(self.__jump_to)
+ self.__tbox.bw_attach_next(self.__info)
+ self.__tbox.bw_attach_next(self.__group)
+ self.__tbox.bw_attach_next(self.__region)
+
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__tbox)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__region_color)
+
+ self.bw_add(self.__vbox)
+
+ self.__jump_to.set_active(True)
+ self.__region_color.set_no_show_all(True)
+ self.__region_color.hide()
+
+ def __change_pointer(self, widget, pointer):
+ """
+ """
+ if pointer != self.radialnet.get_pointer_status():
+ self.radialnet.set_pointer_status(pointer)
+
+ if pointer == RadialNet.POINTER_FILL:
+ self.__region_color.show()
+ else:
+ self.__region_color.hide()
+
+ def __change_region(self, widget):
+ """
+ """
+ self.radialnet.set_region_color(self.__region_color.get_active())
+
+
+class ControlVariableWidget(Gtk.DrawingArea):
+ """
+ """
+ def __init__(self, name, value, update, increment=1):
+ """
+ """
+ Gtk.DrawingArea.__init__(self)
+
+ self.__variable_name = name
+ self.__value = value
+ self.__update = update
+ self.__increment_pass = increment
+
+ self.__radius = 6
+ self.__increment_time = 100
+
+ self.__pointer_position = 0
+ self.__active_increment = False
+
+ self.__last_value = self.__value()
+
+ self.connect('draw', self.draw)
+ self.connect('button_press_event', self.button_press)
+ self.connect('button_release_event', self.button_release)
+ self.connect('motion_notify_event', self.motion_notify)
+
+ self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
+ Gdk.EventMask.POINTER_MOTION_HINT_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK)
+
+ GLib.timeout_add(REFRESH_RATE, self.verify_value)
+
+ def verify_value(self):
+ """
+ """
+ if self.__value() != self.__last_value:
+ self.__last_value = self.__value()
+
+ self.queue_draw()
+
+ return True
+
+ def button_press(self, widget, event):
+ """
+ """
+ self.__active_increment = False
+ pointer = self.get_pointer()
+
+ if self.__button_is_clicked(pointer) and event.button == 1:
+
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
+ self.__active_increment = True
+ self.__increment_value()
+
+ def button_release(self, widget, event):
+ """
+ """
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
+
+ self.__active_increment = False
+ self.__pointer_position = 0
+
+ self.queue_draw()
+
+ def motion_notify(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ if self.__active_increment:
+
+ xc, yc = self.__center_of_widget
+ x, _ = self.get_pointer()
+
+ if x - self.__radius > 0 and x + self.__radius < 2 * xc:
+ self.__pointer_position = x - xc
+
+ self.queue_draw()
+
+ def draw(self, widget, context):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type context: cairo.Context
+ @param context: cairo context class
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ self.set_size_request(100, 30)
+
+ self.__draw(context)
+
+ return True
+
+ def __draw(self, context):
+ """
+ """
+ allocation = self.get_allocation()
+
+ self.__center_of_widget = (allocation.width // 2,
+ allocation.height // 2)
+
+ xc, yc = self.__center_of_widget
+
+ # draw line
+ context.set_line_width(1)
+ context.set_dash([1, 2])
+ context.move_to(self.__radius,
+ yc + self.__radius)
+ context.line_to(2 * xc - 5,
+ yc + self.__radius)
+ context.stroke()
+
+ # draw text
+ context.set_dash([1, 0])
+ context.set_font_size(10)
+
+ context.move_to(5, yc - self.__radius)
+ context.show_text(self.__variable_name)
+
+ width = context.text_extents(str(self.__value()))[2]
+ context.move_to(2 * xc - width - 5, yc - self.__radius)
+ context.show_text(str(self.__value()))
+
+ context.set_line_width(1)
+ context.stroke()
+
+ # draw node
+ context.arc(xc + self.__pointer_position,
+ yc + self.__radius,
+ self.__radius, 0, 2 * math.pi)
+ if self.__active_increment:
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ else:
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill_preserve()
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.stroke()
+
+ def __button_is_clicked(self, pointer):
+ """
+ """
+ xc, yc = self.__center_of_widget
+ center = (xc, yc + self.__radius)
+
+ return geometry.is_in_circle(pointer, 6, center)
+
+ def __increment_value(self):
+ """
+ """
+ self.__update(self.__value() + self.__pointer_position / 4)
+
+ self.queue_draw()
+
+ if self.__active_increment:
+
+ GLib.timeout_add(self.__increment_time,
+ self.__increment_value)
+
+ def set_value_function(self, value):
+ """
+ """
+ self.__value = value
+
+ def set_update_function(self, update):
+ """
+ """
+ self.__update = update
+
+
+class ControlVariable(BWHBox):
+ """
+ """
+ def __init__(self, name, get_function, set_function, increment=1):
+ """
+ """
+ BWHBox.__init__(self, spacing=0)
+
+ self.__increment_pass = increment
+ self.__increment_time = 200
+ self.__increment = False
+
+ self.__name = name
+ self.__get_function = get_function
+ self.__set_function = set_function
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__control = ControlVariableWidget(self.__name,
+ self.__get_function,
+ self.__set_function,
+ self.__increment_pass)
+
+ self.__left_button = Gtk.Button()
+ self.__left_button.set_size_request(20, 20)
+ self.__left_arrow = Gtk.Image.new_from_icon_name("pan-start-symbolic",
+ Gtk.IconSize.BUTTON);
+ self.__left_button.add(self.__left_arrow)
+ self.__left_button.connect('pressed',
+ self.__pressed,
+ -self.__increment_pass)
+ self.__left_button.connect('released', self.__released)
+
+ self.__right_button = Gtk.Button()
+ self.__right_button.set_size_request(20, 20)
+ self.__right_arrow = Gtk.Image.new_from_icon_name("pan-end-symbolic",
+ Gtk.IconSize.BUTTON);
+ self.__right_button.add(self.__right_arrow)
+ self.__right_button.connect('pressed',
+ self.__pressed,
+ self.__increment_pass)
+ self.__right_button.connect('released', self.__released)
+
+ self.bw_pack_start_noexpand_nofill(self.__left_button)
+ self.bw_pack_start_expand_fill(self.__control)
+ self.bw_pack_start_noexpand_nofill(self.__right_button)
+
+ def __pressed(self, widget, increment):
+ """
+ """
+ self.__increment = True
+ self.__increment_function(increment)
+
+ def __increment_function(self, increment):
+ """
+ """
+ if self.__increment:
+
+ self.__set_function(self.__get_function() + increment)
+ self.__control.verify_value()
+
+ GLib.timeout_add(self.__increment_time,
+ self.__increment_function,
+ increment)
+
+ def __released(self, widget):
+ """
+ """
+ self.__increment = False
+
+
+class ControlFisheye(BWVBox):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWVBox.__init__(self)
+ self.set_border_width(6)
+
+ self.radialnet = radialnet
+ self.__ring_max_value = self.radialnet.get_number_of_rings()
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__params = BWHBox()
+
+ self.__fisheye_label = Gtk.Label.new(_('<b>Fisheye</b> on ring'))
+ self.__fisheye_label.set_use_markup(True)
+
+ self.__ring = Gtk.Adjustment.new(0, 0, self.__ring_max_value, 0.01, 0.01, 0)
+
+ self.__ring_spin = Gtk.SpinButton(adjustment=self.__ring)
+ self.__ring_spin.set_digits(2)
+
+ self.__ring_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=self.__ring)
+ self.__ring_scale.set_size_request(100, -1)
+ self.__ring_scale.set_digits(2)
+ self.__ring_scale.set_value_pos(Gtk.PositionType.LEFT)
+ self.__ring_scale.set_draw_value(False)
+
+ self.__interest_label = Gtk.Label.new(_('with interest factor'))
+ self.__interest = Gtk.Adjustment.new(0, 0, 10, 0.01, 0, 0)
+ self.__interest_spin = Gtk.SpinButton(adjustment=self.__interest)
+ self.__interest_spin.set_digits(2)
+
+ self.__spread_label = Gtk.Label.new(_('and spread factor'))
+ self.__spread = Gtk.Adjustment.new(0, -1.0, 1.0, 0.01, 0.01, 0)
+ self.__spread_spin = Gtk.SpinButton(adjustment=self.__spread)
+ self.__spread_spin.set_digits(2)
+
+ self.__params.bw_pack_start_noexpand_nofill(self.__fisheye_label)
+ self.__params.bw_pack_start_noexpand_nofill(self.__ring_spin)
+ self.__params.bw_pack_start_expand_fill(self.__ring_scale)
+ self.__params.bw_pack_start_noexpand_nofill(self.__interest_label)
+ self.__params.bw_pack_start_noexpand_nofill(self.__interest_spin)
+ self.__params.bw_pack_start_noexpand_nofill(self.__spread_label)
+ self.__params.bw_pack_start_noexpand_nofill(self.__spread_spin)
+
+ self.bw_pack_start_noexpand_nofill(self.__params)
+
+ self.__ring.connect('value_changed', self.__change_ring)
+ self.__interest.connect('value_changed', self.__change_interest)
+ self.__spread.connect('value_changed', self.__change_spread)
+
+ GLib.timeout_add(REFRESH_RATE, self.__update_fisheye)
+
+ def __update_fisheye(self):
+ """
+ """
+ # adjust ring scale to radialnet number of nodes
+ ring_max_value = self.radialnet.get_number_of_rings() - 1
+
+ if ring_max_value != self.__ring_max_value:
+
+ value = self.__ring.get_value()
+
+ if value == 0 and ring_max_value != 0:
+ value = 1
+
+ elif value > ring_max_value:
+ value = ring_max_value
+
+ self.__ring.configure(value, 1, ring_max_value, 0.01, 0.01, 0)
+ self.__ring_max_value = ring_max_value
+
+ self.__ring_scale.queue_draw()
+
+ # check ring value
+ ring_value = self.radialnet.get_fisheye_ring()
+
+ if self.__ring.get_value() != ring_value:
+ self.__ring.set_value(ring_value)
+
+ # check interest value
+ interest_value = self.radialnet.get_fisheye_interest()
+
+ if self.__interest.get_value() != interest_value:
+ self.__interest.set_value(interest_value)
+
+ # check spread value
+ spread_value = self.radialnet.get_fisheye_spread()
+
+ if self.__spread.get_value() != spread_value:
+ self.__spread.set_value(spread_value)
+
+ return True
+
+ def active_fisheye(self):
+ """
+ """
+ self.radialnet.set_fisheye(True)
+ self.__change_ring()
+ self.__change_interest()
+
+ def deactive_fisheye(self):
+ """
+ """
+ self.radialnet.set_fisheye(False)
+
+ def __change_ring(self, widget=None):
+ """
+ """
+ if not self.radialnet.is_in_animation():
+ self.radialnet.set_fisheye_ring(self.__ring.get_value())
+ else:
+ self.__ring.set_value(self.radialnet.get_fisheye_ring())
+
+ def __change_interest(self, widget=None):
+ """
+ """
+ if not self.radialnet.is_in_animation():
+ self.radialnet.set_fisheye_interest(self.__interest.get_value())
+ else:
+ self.__interest.set_value(self.radialnet.get_fisheye_interest())
+
+ def __change_spread(self, widget=None):
+ """
+ """
+ if not self.radialnet.is_in_animation():
+ self.radialnet.set_fisheye_spread(self.__spread.get_value())
+ else:
+ self.__spread.set_value(self.radialnet.get_fisheye_spread())
+
+
+class ControlInterpolation(BWExpander):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWExpander.__init__(self, _('Interpolation'))
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__vbox = BWVBox()
+
+ self.__cartesian_radio = Gtk.RadioButton(group=None,
+ label=_('Cartesian'))
+ self.__polar_radio = Gtk.RadioButton(group=self.__cartesian_radio,
+ label=_('Polar'))
+ self.__cartesian_radio.connect('toggled',
+ self.__change_system,
+ RadialNet.INTERPOLATION_CARTESIAN)
+ self.__polar_radio.connect('toggled',
+ self.__change_system,
+ RadialNet.INTERPOLATION_POLAR)
+
+ self.__system_box = BWHBox()
+ self.__system_box.bw_pack_start_noexpand_nofill(self.__polar_radio)
+ self.__system_box.bw_pack_start_noexpand_nofill(self.__cartesian_radio)
+
+ self.__frames_box = BWHBox()
+ self.__frames_label = Gtk.Label.new(_('Frames'))
+ self.__frames_label.set_alignment(0.0, 0.5)
+ self.__frames = Gtk.Adjustment.new(
+ self.radialnet.get_number_of_frames(), 1, 1000, 1, 0, 0)
+ self.__frames.connect('value_changed', self.__change_frames)
+ self.__frames_spin = Gtk.SpinButton(adjustment=self.__frames)
+ self.__frames_box.bw_pack_start_expand_fill(self.__frames_label)
+ self.__frames_box.bw_pack_start_noexpand_nofill(self.__frames_spin)
+
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__frames_box)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__system_box)
+
+ self.bw_add(self.__vbox)
+
+ GLib.timeout_add(REFRESH_RATE, self.__update_animation)
+
+ def __update_animation(self):
+ """
+ """
+ active = self.radialnet.get_interpolation()
+
+ if active == RadialNet.INTERPOLATION_CARTESIAN:
+ self.__cartesian_radio.set_active(True)
+
+ else:
+ self.__polar_radio.set_active(True)
+
+ return True
+
+ def __change_system(self, widget, value):
+ """
+ """
+ if not self.radialnet.set_interpolation(value):
+
+ active = self.radialnet.get_interpolation()
+
+ if active == RadialNet.INTERPOLATION_CARTESIAN:
+ self.__cartesian_radio.set_active(True)
+
+ else:
+ self.__polar_radio.set_active(True)
+
+ def __change_frames(self, widget):
+ """
+ """
+ if not self.radialnet.set_number_of_frames(self.__frames.get_value()):
+ self.__frames.set_value(self.radialnet.get_number_of_frames())
+
+
+class ControlLayout(BWExpander):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWExpander.__init__(self, _('Layout'))
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__hbox = BWHBox()
+
+ self.__layout = Gtk.ComboBoxText()
+ self.__layout.append_text(_('Symmetric'))
+ self.__layout.append_text(_('Weighted'))
+ self.__layout.set_active(self.radialnet.get_layout())
+ self.__layout.connect('changed', self.__change_layout)
+ self.__force = Gtk.ToolButton(stock_id=Gtk.STOCK_REFRESH)
+ self.__force.connect('clicked', self.__force_update)
+
+ self.__hbox.bw_pack_start_expand_fill(self.__layout)
+ self.__hbox.bw_pack_start_noexpand_nofill(self.__force)
+
+ self.bw_add(self.__hbox)
+
+ self.__check_layout()
+
+ def __check_layout(self):
+ """
+ """
+ if self.__layout.get_active() == RadialNet.LAYOUT_WEIGHTED:
+ self.__force.set_sensitive(True)
+
+ else:
+ self.__force.set_sensitive(False)
+
+ return True
+
+ def __force_update(self, widget):
+ """
+ """
+ self.__fisheye_ring = self.radialnet.get_fisheye_ring()
+ self.radialnet.update_layout()
+
+ def __change_layout(self, widget):
+ """
+ """
+ if not self.radialnet.set_layout(self.__layout.get_active()):
+ self.__layout.set_active(self.radialnet.get_layout())
+
+ else:
+ self.__check_layout()
+
+
+class ControlRingGap(BWVBox):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWVBox.__init__(self)
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__radius = ControlVariable(_('Ring gap'),
+ self.radialnet.get_ring_gap,
+ self.radialnet.set_ring_gap)
+
+ self.__label = Gtk.Label.new(_('Lower ring gap'))
+ self.__label.set_alignment(0.0, 0.5)
+ self.__adjustment = Gtk.Adjustment.new(
+ self.radialnet.get_min_ring_gap(), 0, 50, 1, 0, 0)
+ self.__spin = Gtk.SpinButton(adjustment=self.__adjustment)
+ self.__spin.connect('value_changed', self.__change_lower)
+
+ self.__lower_hbox = BWHBox()
+ self.__lower_hbox.bw_pack_start_expand_fill(self.__label)
+ self.__lower_hbox.bw_pack_start_noexpand_nofill(self.__spin)
+
+ self.bw_pack_start_noexpand_nofill(self.__radius)
+ self.bw_pack_start_noexpand_nofill(self.__lower_hbox)
+
+ def __change_lower(self, widget):
+ """
+ """
+ if not self.radialnet.set_min_ring_gap(self.__adjustment.get_value()):
+ self.__adjustment.set_value(self.radialnet.get_min_ring_gap())
+
+
+class ControlOptions(BWScrolledWindow):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWScrolledWindow.__init__(self)
+
+ self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
+ self.set_shadow_type(Gtk.ShadowType.NONE)
+
+ self.radialnet = radialnet
+
+ self.enable_labels = True
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__liststore = Gtk.ListStore.new([bool, str])
+
+ self.__liststore.append([None, OPTIONS[0]])
+ self.__liststore.append([None, OPTIONS[1]])
+ self.__liststore.append([None, OPTIONS[2]])
+ self.__liststore.append([None, OPTIONS[3]])
+ self.__liststore.append([None, OPTIONS[4]])
+ self.__liststore.append([None, OPTIONS[5]])
+ self.__liststore.append([None, OPTIONS[6]])
+
+ self.__cell_toggle = Gtk.CellRendererToggle()
+ self.__cell_toggle.set_property('activatable', True)
+ self.__cell_toggle.connect('toggled',
+ self.__change_option,
+ self.__liststore)
+
+ self.__column_toggle = Gtk.TreeViewColumn(cell_renderer=self.__cell_toggle)
+ self.__column_toggle.add_attribute(self.__cell_toggle, 'active', 0)
+ self.__column_toggle.set_cell_data_func(self.__cell_toggle, self.__cell_toggle_data_method)
+
+ self.__cell_text = Gtk.CellRendererText()
+
+ self.__column_text = Gtk.TreeViewColumn(title=None,
+ cell_renderer=self.__cell_text,
+ text=1)
+ self.__column_text.set_cell_data_func(self.__cell_text, self.__cell_text_data_method)
+
+ self.__treeview = Gtk.TreeView.new_with_model(self.__liststore)
+ self.__treeview.set_enable_search(True)
+ self.__treeview.set_search_column(1)
+ self.__treeview.set_headers_visible(False)
+ self.__treeview.append_column(self.__column_toggle)
+ self.__treeview.append_column(self.__column_text)
+
+ self.add_with_viewport(self.__treeview)
+
+ GLib.timeout_add(REFRESH_RATE, self.__update_options)
+
+ def __cell_toggle_data_method(self, column, cell, model, it, data):
+ if not self.enable_labels and model.get_value(it, 1) == 'hostname':
+ cell.set_property('activatable', False)
+ else:
+ cell.set_property('activatable', True)
+
+ def __cell_text_data_method(self, column, cell, model, it, data):
+ if not self.enable_labels and model.get_value(it, 1) == 'hostname':
+ cell.set_property('strikethrough', True)
+ else:
+ cell.set_property('strikethrough', False)
+
+ def __update_options(self):
+ """
+ """
+ model = self.__liststore
+
+ model[OPTIONS.index('address')][0] = self.radialnet.get_show_address()
+ model[OPTIONS.index('hostname')][0] = self.enable_labels and \
+ self.radialnet.get_show_hostname()
+ model[OPTIONS.index('icon')][0] = self.radialnet.get_show_icon()
+ model[OPTIONS.index('latency')][0] = self.radialnet.get_show_latency()
+ model[OPTIONS.index('ring')][0] = self.radialnet.get_show_ring()
+ model[OPTIONS.index('region')][0] = self.radialnet.get_show_region()
+ model[OPTIONS.index('slow in/out')][0] = \
+ self.radialnet.get_slow_inout()
+
+ return True
+
+ def __change_option(self, cell, option, model):
+ """
+ """
+ option = int(option)
+ model[option][0] = not model[option][0]
+
+ if OPTIONS[option] == 'address':
+ self.radialnet.set_show_address(model[option][0])
+ if model[option][0]:
+ model[OPTIONS.index('hostname')][0] = self.radialnet.get_show_hostname()
+ else:
+ model[OPTIONS.index('hostname')][0] = False
+ self.enable_labels = model[option][0]
+
+ elif OPTIONS[option] == 'hostname':
+ self.radialnet.set_show_hostname(model[option][0])
+
+ elif OPTIONS[option] == 'icon':
+ self.radialnet.set_show_icon(model[option][0])
+
+ elif OPTIONS[option] == 'latency':
+ self.radialnet.set_show_latency(model[option][0])
+
+ elif OPTIONS[option] == 'ring':
+ self.radialnet.set_show_ring(model[option][0])
+
+ elif OPTIONS[option] == 'region':
+ self.radialnet.set_show_region(model[option][0])
+
+ elif OPTIONS[option] == 'slow in/out':
+ self.radialnet.set_slow_inout(model[option][0])
+
+
+class ControlView(BWExpander):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ BWExpander.__init__(self, _('View'))
+ self.set_expanded(True)
+
+ self.radialnet = radialnet
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__vbox = BWVBox(spacing=0)
+
+ self.__zoom = ControlVariable(_('Zoom'),
+ self.radialnet.get_zoom,
+ self.radialnet.set_zoom)
+
+ self.__ring_gap = ControlRingGap(self.radialnet)
+ self.__navigation = ControlNavigation(self.radialnet)
+
+ self.__options = ControlOptions(self.radialnet)
+ self.__options.set_border_width(0)
+
+ self.__vbox.bw_pack_start_expand_nofill(self.__options)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__navigation)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__zoom)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__ring_gap)
+
+ self.bw_add(self.__vbox)
+
+
+class ControlNavigation(Gtk.DrawingArea):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ Gtk.DrawingArea.__init__(self)
+
+ self.radialnet = radialnet
+
+ self.__rotate_node = PolarCoordinate()
+ self.__rotate_node.set_coordinate(40, 90)
+ self.__center_of_widget = (50, 50)
+ self.__moving = None
+ self.__centering = False
+ self.__rotating = False
+ self.__move_pass = 100
+
+ self.__move_position = (0, 0)
+ self.__move_addition = [(-1, 0),
+ (-1, -1),
+ (0, -1),
+ (1, -1),
+ (1, 0),
+ (1, 1),
+ (0, 1),
+ (-1, 1)]
+
+ self.__move_factor = 1
+ self.__move_factor_limit = 20
+
+ self.__rotate_radius = 6
+ self.__move_radius = 6
+
+ self.__rotate_clicked = False
+ self.__move_clicked = None
+
+ self.connect('draw', self.draw)
+ self.connect('button_press_event', self.button_press)
+ self.connect('button_release_event', self.button_release)
+ self.connect('motion_notify_event', self.motion_notify)
+ self.connect('enter_notify_event', self.enter_notify)
+ self.connect('leave_notify_event', self.leave_notify)
+ self.connect('key_press_event', self.key_press)
+ self.connect('key_release_event', self.key_release)
+
+ self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
+ Gdk.EventMask.ENTER_NOTIFY_MASK |
+ Gdk.EventMask.LEAVE_NOTIFY_MASK |
+ Gdk.EventMask.KEY_PRESS_MASK |
+ Gdk.EventMask.KEY_RELEASE_MASK |
+ Gdk.EventMask.POINTER_MOTION_HINT_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK)
+
+ self.__rotate_node.set_coordinate(40, self.radialnet.get_rotation())
+
+ def key_press(self, widget, event):
+ """
+ """
+ # key = Gdk.keyval_name(event.keyval)
+
+ self.queue_draw()
+
+ return True
+
+ def key_release(self, widget, event):
+ """
+ """
+ # key = Gdk.keyval_name(event.keyval)
+
+ self.queue_draw()
+
+ return True
+
+ def enter_notify(self, widget, event):
+ """
+ """
+ return False
+
+ def leave_notify(self, widget, event):
+ """
+ """
+ self.queue_draw()
+
+ return False
+
+ def button_press(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ pointer = self.get_pointer()
+
+ direction = False
+
+ if self.__rotate_is_clicked(pointer):
+
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
+ self.__rotating = True
+
+ direction = self.__move_is_clicked(pointer)
+
+ if direction is not None and self.__moving is None:
+
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
+ self.__moving = direction
+ self.__move_in_direction(direction)
+
+ if self.__center_is_clicked(pointer):
+
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
+ self.__centering = True
+ self.__move_position = (0, 0)
+ self.radialnet.set_translation(self.__move_position)
+
+ self.queue_draw()
+
+ return False
+
+ def button_release(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ self.__moving = None # stop moving
+ self.__centering = False
+ self.__rotating = False # stop rotate
+ self.__move_factor = 1
+
+ event.window.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
+
+ self.queue_draw()
+
+ return False
+
+ def motion_notify(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ xc, yc = self.__center_of_widget
+ x, y = self.get_pointer()
+
+ status = not self.radialnet.is_in_animation()
+ status = status and not self.radialnet.is_empty()
+
+ if self.__rotating and status:
+
+ r, t = self.__rotate_node.get_coordinate()
+ t = math.degrees(math.atan2(yc - y, x - xc))
+
+ if t < 0:
+ t = 360 + t
+
+ self.radialnet.set_rotation(t)
+ self.__rotate_node.set_coordinate(r, t)
+
+ self.queue_draw()
+
+ return False
+
+ def draw(self, widget, context):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type context: cairo.Context
+ @param context: cairo context class
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ self.set_size_request(120, 130)
+
+ self.__draw(context)
+
+ return False
+
+ def __draw_rotate_control(self, context):
+ """
+ """
+ xc, yc = self.__center_of_widget
+ r, t = self.__rotate_node.get_coordinate()
+ x, y = self.__rotate_node.to_cartesian()
+
+ # draw text
+ context.set_font_size(10)
+ context.move_to(xc - 49, yc - 48)
+ context.show_text(_("Navigation"))
+
+ width = context.text_extents(str(int(t)))[2]
+ context.move_to(xc + 49 - width - 2, yc - 48)
+ context.show_text(str(round(t, 1)))
+ context.set_line_width(1)
+ context.stroke()
+
+ # draw arc
+ context.set_dash([1, 2])
+ context.arc(xc, yc, 40, 0, 2 * math.pi)
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_line_width(1)
+ context.stroke()
+
+ # draw node
+ context.set_dash([1, 0])
+ context.arc(xc + x, yc - y, self.__rotate_radius, 0, 2 * math.pi)
+
+ if self.__rotating:
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ else:
+ context.set_source_rgb(1.0, 1.0, 1.0)
+
+ context.fill_preserve()
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_line_width(1)
+ context.stroke()
+
+ return False
+
+ def __draw_move_control(self, context):
+ """
+ """
+ xc, yc = self.__center_of_widget
+ pc = PolarCoordinate()
+
+ context.set_dash([1, 1])
+ context.arc(xc, yc, 23, 0, 2 * math.pi)
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_line_width(1)
+ context.stroke()
+
+ for i in range(8):
+
+ pc.set_coordinate(23, 45 * i)
+ x, y = pc.to_cartesian()
+
+ context.set_dash([1, 1])
+ context.move_to(xc, yc)
+ context.line_to(xc + x, yc - y)
+ context.stroke()
+
+ context.set_dash([1, 0])
+ context.arc(
+ xc + x, yc - y, self.__move_radius, 0, 2 * math.pi)
+
+ if i == self.__moving:
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ else:
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill_preserve()
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_line_width(1)
+ context.stroke()
+
+ context.arc(xc, yc, 6, 0, 2 * math.pi)
+
+ if self.__centering:
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ else:
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill_preserve()
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_line_width(1)
+ context.stroke()
+
+ return False
+
+ def __draw(self, context):
+ """
+ Drawing method
+ """
+ # Getting allocation reference
+ allocation = self.get_allocation()
+
+ self.__center_of_widget = (allocation.width // 2,
+ allocation.height // 2)
+
+ self.__draw_rotate_control(context)
+ self.__draw_move_control(context)
+
+ return False
+
+ def __move_in_direction(self, direction):
+ """
+ """
+ if self.__moving is not None:
+
+ bx, by = self.__move_position
+ ax, ay = self.__move_addition[direction]
+
+ self.__move_position = (bx + self.__move_factor * ax,
+ by + self.__move_factor * ay)
+ self.radialnet.set_translation(self.__move_position)
+
+ if self.__move_factor < self.__move_factor_limit:
+ self.__move_factor += 1
+
+ GLib.timeout_add(self.__move_pass,
+ self.__move_in_direction,
+ direction)
+
+ return False
+
+ def __rotate_is_clicked(self, pointer):
+ """
+ """
+ xn, yn = self.__rotate_node.to_cartesian()
+ xc, yc = self.__center_of_widget
+
+ center = (xc + xn, yc - yn)
+ return geometry.is_in_circle(pointer, self.__rotate_radius, center)
+
+ def __center_is_clicked(self, pointer):
+ """
+ """
+ return geometry.is_in_circle(pointer, self.__move_radius,
+ self.__center_of_widget)
+
+ def __move_is_clicked(self, pointer):
+ """
+ """
+ xc, yc = self.__center_of_widget
+ pc = PolarCoordinate()
+
+ for i in range(8):
+
+ pc.set_coordinate(23, 45 * i)
+ x, y = pc.to_cartesian()
+
+ center = (xc + x, yc - y)
+ if geometry.is_in_circle(pointer, self.__move_radius, center):
+ return i
+
+ return None
diff --git a/zenmap/radialnet/gui/Dialogs.py b/zenmap/radialnet/gui/Dialogs.py
new file mode 100644
index 0000000..01d97f2
--- /dev/null
+++ b/zenmap/radialnet/gui/Dialogs.py
@@ -0,0 +1,89 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+from radialnet.core.Info import INFO
+from radialnet.gui.Image import Pixmaps
+
+
+class AboutDialog(Gtk.AboutDialog):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ Gtk.AboutDialog.__init__(self)
+
+ self.set_name(INFO['name'])
+ self.set_version(INFO['version'])
+ self.set_website(INFO['website'])
+ self.set_authors(INFO['authors'])
+ self.set_license(INFO['license'])
+ self.set_copyright(INFO['copyright'])
+
+ self.set_logo(Pixmaps().get_pixbuf('logo'))
+
+ self.connect('response', self.__destroy)
+
+ def __destroy(self, dialog, id):
+ """
+ """
+ self.destroy()
diff --git a/zenmap/radialnet/gui/HostsViewer.py b/zenmap/radialnet/gui/HostsViewer.py
new file mode 100644
index 0000000..feb0ae8
--- /dev/null
+++ b/zenmap/radialnet/gui/HostsViewer.py
@@ -0,0 +1,231 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+import re
+
+from radialnet.bestwidgets.windows import BWMainWindow
+
+from radialnet.gui.NodeNotebook import NodeNotebook
+from radialnet.util.misc import ipv4_compare
+
+
+HOSTS_COLORS = ['#d5ffd5', '#ffffd5', '#ffd5d5']
+
+HOSTS_HEADER = ['ID', '#', 'Hosts']
+
+DIMENSION = (700, 400)
+
+IP_RE = re.compile(r'^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$')
+
+
+class HostsViewer(BWMainWindow):
+ """
+ """
+ def __init__(self, nodes):
+ """
+ """
+ BWMainWindow.__init__(self)
+ self.set_title(_('Hosts Viewer'))
+ self.set_default_size(DIMENSION[0], DIMENSION[1])
+
+ self.__nodes = nodes
+ self.__default_view = Gtk.Label.new(_("No node selected"))
+ self.__view = self.__default_view
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__panel = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
+ self.__panel.set_border_width(6)
+
+ self.__list = HostsList(self, self.__nodes)
+
+ self.__panel.add1(self.__list)
+ self.__panel.add2(self.__view)
+ self.__panel.set_position(int(DIMENSION[0] / 5))
+
+ self.add(self.__panel)
+
+ def change_notebook(self, node):
+ """
+ """
+ if self.__view is not None:
+ self.__view.destroy()
+
+ if node is not None:
+ self.__view = NodeNotebook(node)
+ else:
+ self.__view = self.__default_view
+ self.__view.show_all()
+
+ self.__panel.add2(self.__view)
+
+
+class HostsList(Gtk.ScrolledWindow):
+ """
+ """
+ def __init__(self, parent, nodes):
+ """
+ """
+ super(HostsList, self).__init__()
+ self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ self.set_shadow_type(Gtk.ShadowType.NONE)
+
+ self.__parent = parent
+ self.__nodes = nodes
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__cell = Gtk.CellRendererText()
+
+ self.__hosts_store = Gtk.ListStore.new([int, int, str, str, bool])
+
+ self.__hosts_treeview = Gtk.TreeView.new_with_model(self.__hosts_store)
+ self.__hosts_treeview.connect('cursor-changed', self.__cursor_callback)
+
+ for i in range(len(self.__nodes)):
+
+ node = self.__nodes[i]
+
+ ports = node.get_info('number_of_open_ports')
+ color = HOSTS_COLORS[node.get_info('vulnerability_score')]
+
+ host = node.get_info('hostname') or node.get_info('ip') or ""
+
+ self.__hosts_store.append([i,
+ ports,
+ host,
+ color,
+ True])
+
+ self.__hosts_column = list()
+
+ for i in range(0, len(HOSTS_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=HOSTS_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__hosts_column.append(column)
+
+ self.__hosts_column[i].set_reorderable(True)
+ self.__hosts_column[i].set_resizable(True)
+ self.__hosts_column[i].set_attributes(self.__cell,
+ text=i,
+ background=3,
+ editable=4)
+
+ self.__hosts_treeview.append_column(self.__hosts_column[2])
+
+ self.__hosts_store.set_sort_func(2, self.__host_sort)
+
+ self.__hosts_column[2].set_sort_column_id(2)
+
+ self.add_with_viewport(self.__hosts_treeview)
+
+ if len(self.__hosts_treeview.get_model()) > 0:
+ self.__hosts_treeview.set_cursor((0,))
+ self.__cursor_callback(self.__hosts_treeview)
+
+ def __cursor_callback(self, widget):
+ """
+ """
+ path = widget.get_cursor()[0]
+ if path is None:
+ return
+
+ iter = self.__hosts_store.get_iter(path)
+
+ node = self.__nodes[self.__hosts_store.get_value(iter, 0)]
+
+ self.__parent.change_notebook(node)
+
+ def __host_sort(self, treemodel, iter1, iter2, *_):
+ """
+ """
+ value1 = treemodel.get_value(iter1, 2)
+ value2 = treemodel.get_value(iter2, 2)
+
+ value1_is_ip = IP_RE.match(value1)
+ value2_is_ip = IP_RE.match(value2)
+
+ if value1_is_ip and value2_is_ip:
+ return ipv4_compare(value1, value2)
+
+ if value1_is_ip:
+ return -1
+
+ if value2_is_ip:
+ return 1
+
+ if value1 < value2:
+ return -1
+
+ if value1 > value2:
+ return 1
+
+ return 0
diff --git a/zenmap/radialnet/gui/Image.py b/zenmap/radialnet/gui/Image.py
new file mode 100644
index 0000000..72020ca
--- /dev/null
+++ b/zenmap/radialnet/gui/Image.py
@@ -0,0 +1,164 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import GdkPixbuf
+
+import os
+import array
+
+from zenmapCore.Paths import Path
+
+
+FORMAT_RGBA = 4
+FORMAT_RGB = 3
+
+
+def get_pixels_for_cairo_image_surface(pixbuf):
+ """
+ This method return the image stride and a python array.ArrayType
+ containing the icon pixels of a gtk.gdk.Pixbuf that can be used by
+ cairo.ImageSurface.create_for_data() method.
+ """
+ data = array.array('B')
+ image_format = pixbuf.get_rowstride() // pixbuf.get_width()
+
+ i = 0
+ j = 0
+ while i < len(pixbuf.get_pixels()):
+
+ b, g, r = pixbuf.get_pixels()[i:i + FORMAT_RGB]
+
+ if image_format == FORMAT_RGBA:
+ a = pixbuf.get_pixels()[i + FORMAT_RGBA - 1]
+ elif image_format == FORMAT_RGB:
+ a = 255
+ else:
+ raise TypeError('unknown image format')
+
+ data[j:j + FORMAT_RGBA] = array.array('B', [r, g, b, a])
+
+ i += image_format
+ j += FORMAT_RGBA
+
+ return (FORMAT_RGBA * pixbuf.get_width(), data)
+
+
+class Image:
+ """
+ """
+ def __init__(self, path=None):
+ """
+ """
+ self.__path = path
+ self.__cache = dict()
+
+ def set_path(self, path):
+ """
+ """
+ self.__path = path
+
+ def get_pixbuf(self, icon, image_type='png'):
+ """
+ """
+ if self.__path is None:
+ return False
+
+ if icon + image_type not in self.__cache.keys():
+
+ file = self.get_icon(icon, image_type)
+ self.__cache[icon + image_type] = \
+ GdkPixbuf.Pixbuf.new_from_file(file)
+
+ return self.__cache[icon + image_type]
+
+ def get_icon(self, icon, image_type='png'):
+ """
+ """
+ if self.__path is None:
+ return False
+
+ return os.path.join(self.__path, icon + "." + image_type)
+
+
+class Pixmaps(Image):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ Image.__init__(self, os.path.join(Path.pixmaps_dir, "radialnet"))
+
+
+class Icons(Image):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ Image.__init__(self, os.path.join(Path.pixmaps_dir, "radialnet"))
+
+
+class Application(Image):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ Image.__init__(self, os.path.join(Path.pixmaps_dir, "radialnet"))
diff --git a/zenmap/radialnet/gui/LegendWindow.py b/zenmap/radialnet/gui/LegendWindow.py
new file mode 100644
index 0000000..3401e43
--- /dev/null
+++ b/zenmap/radialnet/gui/LegendWindow.py
@@ -0,0 +1,242 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, Gdk, Pango
+
+import math
+import cairo
+
+import zenmapCore.I18N # lgtm[py/unused-import]
+
+from radialnet.gui.Image import Pixmaps
+DIMENSION_NORMAL = (350, 450)
+
+
+def draw_pixmap(context, x, y, name, label):
+ # This is pretty hideous workaround
+ Gdk.cairo_set_source_pixbuf(context, Pixmaps().get_pixbuf(name), x, y)
+ #context.set_source_pixbuf()
+ context.paint()
+ context.move_to(x + 50, y + 10)
+ context.set_source_rgb(0, 0, 0)
+ context.show_text(label)
+
+
+def reset_font(context):
+ context.select_font_face(
+ "Monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_SLANT_NORMAL)
+ context.set_font_size(11)
+
+
+def draw_heading(context, x, y, label):
+ context.select_font_face(
+ "Monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
+ context.set_font_size(13)
+ context.move_to(x - 15, y)
+ context.set_source_rgb(0, 0, 0)
+ context.show_text(label)
+ reset_font(context)
+
+
+def draw_circle(context, x, y, size, color, label):
+ context.set_source_rgb(0, 0, 0)
+ context.move_to(x, y)
+ context.arc(x, y, size, 0, 2 * math.pi)
+ context.stroke_preserve()
+ context.set_source_rgb(*color)
+ context.fill()
+ context.set_source_rgb(0, 0, 0)
+ context.move_to(x + 50, y + 5)
+ context.show_text(label)
+
+
+def draw_square(context, x, y, size, color):
+ context.set_source_rgb(0, 0, 0)
+ context.rectangle(x, y - size / 2, size, size)
+ context.stroke_preserve()
+ context.set_source_rgb(*color)
+ context.fill()
+
+
+def draw_line(context, x, y, dash, color, label):
+ context.set_source_rgb(*color)
+ context.move_to(x - 20, y)
+ context.set_dash(dash)
+ context.line_to(x + 25, y)
+ context.stroke()
+ context.set_dash([])
+ context.set_source_rgb(0, 0, 0)
+ context.move_to(x + 50, y + 5)
+ context.show_text(label)
+
+
+class LegendWindow(Gtk.Window):
+ """
+ """
+ def __init__(self):
+ """
+ """
+ Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)
+ self.set_default_size(DIMENSION_NORMAL[0], DIMENSION_NORMAL[1])
+ self.__title_font = Pango.FontDescription("Monospace Bold")
+ self.set_title(_("Topology Legend"))
+
+ self.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
+ self.add(self.vbox)
+
+ self.drawing_area = Gtk.DrawingArea()
+ self.vbox.pack_start(self.drawing_area, True, True, 0)
+ self.drawing_area.connect("draw", self.draw_event_handler)
+ self.more_uri = Gtk.LinkButton.new_with_label(
+ "https://nmap.org/book/zenmap-topology.html#zenmap-topology-legend",
+ _("View full legend online"))
+ self.vbox.pack_start(self.more_uri, False, False, 0)
+
+ def draw_event_handler(self, widget, graphic_context):
+ """
+ """
+ x, y = 45, 20
+ draw_heading(graphic_context, x, y, _("Hosts"))
+
+ # white circle
+ y += 20
+ draw_circle(graphic_context, x, y, 3, (1, 1, 1),
+ _("host was not port scanned"))
+ # green circle
+ y += 20
+ draw_circle(graphic_context, x, y, 4, (0, 1, 0),
+ _("host with fewer than 3 open ports"))
+ # yellow circle
+ y += 20
+ draw_circle(graphic_context, x, y, 5, (1, 1, 0),
+ _("host with 3 to 6 open ports"))
+ # red circle
+ y += 20
+ draw_circle(graphic_context, x, y, 6, (1, 0, 0),
+ _("host with more than 6 open ports"))
+
+ # green square
+ y += 20
+ rx = x - 20
+ draw_square(graphic_context, rx, y, 10, (0, 1, 0))
+ rx += 10 + 5
+ # yellow square
+ draw_square(graphic_context, rx, y, 12, (1, 1, 0))
+ rx += 12 + 5
+ # red square
+ draw_square(graphic_context, rx, y, 14, (1, 0, 0))
+
+ graphic_context.move_to(x + 50, y + 5)
+ graphic_context.set_source_rgb(0, 0, 0)
+ graphic_context.show_text(_("host is a router, switch, or WAP"))
+
+ # connections between hosts
+ y += 30
+ draw_heading(graphic_context, x, y, _("Traceroute connections"))
+
+ y += 20
+ graphic_context.move_to(x, y)
+ graphic_context.show_text(_("Thicker line means higher round-trip time"))
+ # primary traceroute (blue line)
+ y += 20
+ draw_line(graphic_context, x, y, [], (0, 0, 1),
+ _("primary traceroute connection"))
+ # Alternate route (orange line)
+ y += 20
+ draw_line(graphic_context, x, y, [], (1, 0.5, 0),
+ _("alternate path"))
+ # no traceroute
+ y += 20
+ draw_line(graphic_context, x, y, [4.0, 2.0], (0, 0, 0),
+ _("no traceroute information"))
+ # missing traceroute
+ y += 20
+ graphic_context.set_source_rgb(0.5, 0.7, 0.95)
+ graphic_context.move_to(x - 15, y)
+ graphic_context.arc(x - 25, y, 5, 0, 2 * math.pi)
+ graphic_context.stroke_preserve()
+ draw_line(graphic_context, x, y, [4.0, 2.0], (0.5, 0.7, 0.95),
+ _("missing traceroute hop"))
+
+ # special purpose hosts
+ y += 30
+ draw_heading(graphic_context, x, y, _("Additional host icons"))
+
+ # router image
+ y += 20
+ draw_pixmap(graphic_context, x, y, "router", _("router"))
+
+ # switch image
+ y += 20
+ draw_pixmap(graphic_context, x, y, "switch", _("switch"))
+
+ # wap image
+ y += 20
+ draw_pixmap(graphic_context, x, y, "wireless",
+ _("wireless access point"))
+
+ # firewall image
+ y += 20
+ draw_pixmap(graphic_context, x, y, "firewall", _("firewall"))
+
+ # host with filtered ports
+ y += 20
+ draw_pixmap(graphic_context, x, y, "padlock",
+ _("host with some filtered ports"))
diff --git a/zenmap/radialnet/gui/NodeNotebook.py b/zenmap/radialnet/gui/NodeNotebook.py
new file mode 100644
index 0000000..ea208ec
--- /dev/null
+++ b/zenmap/radialnet/gui/NodeNotebook.py
@@ -0,0 +1,742 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GObject, Pango
+
+from radialnet.bestwidgets.boxes import BWVBox, BWHBox, BWScrolledWindow, BWTable
+from radialnet.bestwidgets.expanders import BWExpander
+from radialnet.bestwidgets.labels import BWLabel, BWSectionLabel
+from radialnet.bestwidgets.textview import BWTextEditor
+import zenmapCore.I18N # lgtm[py/unused-import]
+
+
+PORTS_HEADER = [
+ _('Port'), _('Protocol'), _('State'), _('Service'), _('Method')]
+EXTRAPORTS_HEADER = [_('Count'), _('State'), _('Reasons')]
+
+SERVICE_COLORS = {'open': '#ffd5d5', # noqa
+ 'closed': '#d5ffd5', # noqa
+ 'filtered': '#ffffd5', # noqa
+ 'unfiltered': '#ffd5d5', # noqa
+ 'open|filtered': '#ffd5d5', # noqa
+ 'closed|filtered': '#d5ffd5'} # noqa
+UNKNOWN_SERVICE_COLOR = '#d5d5d5'
+
+TRACE_HEADER = ['TTL', 'RTT', 'IP', _('Hostname')]
+
+TRACE_TEXT = _(
+ "Traceroute on port <b>%s/%s</b> totalized <b>%d</b> known hops.")
+
+NO_TRACE_TEXT = _("No traceroute information available.")
+
+HOP_COLOR = {'known': '#ffffff', # noqa
+ 'unknown': '#cccccc'} # noqa
+
+SYSTEM_ADDRESS_TEXT = "[%s] %s"
+
+OSMATCH_HEADER = ['%', _('Name'), _('DB Line')]
+OSCLASS_HEADER = ['%', _('Vendor'), _('Type'), _('Family'), _('Version')]
+
+USED_PORTS_TEXT = "%d/%s %s"
+
+TCP_SEQ_NOTE = _("""\
+<b>*</b> TCP sequence <i>index</i> equal to %d and <i>difficulty</i> is "%s".\
+""")
+
+
+def get_service_color(state):
+ color = SERVICE_COLORS.get(state)
+ if color is None:
+ color = UNKNOWN_SERVICE_COLOR
+ return color
+
+
+class NodeNotebook(Gtk.Notebook):
+ """
+ """
+ def __init__(self, node):
+ """
+ """
+ Gtk.Notebook.__init__(self)
+ self.set_tab_pos(Gtk.PositionType.TOP)
+
+ self.__node = node
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ # create body elements
+ self.__services_page = ServicesPage(self.__node)
+ self.__system_page = SystemPage(self.__node)
+ self.__trace_page = TraceroutePage(self.__node)
+
+ # packing notebook elements
+ self.append_page(self.__system_page, BWLabel(_('General')))
+ self.append_page(self.__services_page, BWLabel(_('Services')))
+ self.append_page(self.__trace_page, BWLabel(_('Traceroute')))
+
+
+class ServicesPage(Gtk.Notebook):
+ """
+ """
+ def __init__(self, node):
+ """
+ """
+ Gtk.Notebook.__init__(self)
+ self.set_border_width(6)
+ self.set_tab_pos(Gtk.PositionType.TOP)
+
+ self.__node = node
+ self.__font = Pango.FontDescription('Monospace')
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__cell = Gtk.CellRendererText()
+
+ # texteditor widgets
+ self.__texteditor = BWTextEditor()
+ self.__texteditor.bw_modify_font(self.__font)
+ self.__texteditor.bw_set_editable(False)
+ self.__texteditor.set_border_width(0)
+
+ self.__select_combobox = Gtk.ComboBoxText()
+ self.__select_combobox.connect('changed', self.__change_text_value)
+
+ self.__viewer = BWVBox(spacing=6)
+ self.__viewer.set_border_width(6)
+
+ self.__viewer.bw_pack_start_noexpand_nofill(self.__select_combobox)
+ self.__viewer.bw_pack_start_expand_fill(self.__texteditor)
+
+ self.__text = list()
+
+ # ports information
+ number_of_ports = len(self.__node.get_info('ports'))
+ self.__ports_label = BWLabel(_('Ports (%s)') % number_of_ports)
+
+ self.__ports_scroll = BWScrolledWindow()
+
+ self.__ports_store = Gtk.TreeStore.new([GObject.TYPE_INT,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_BOOLEAN])
+
+ self.__ports_treeview = Gtk.TreeView.new_with_model(self.__ports_store)
+
+ for port in self.__node.get_info('ports'):
+
+ color = get_service_color(port['state']['state'])
+
+ service_name = port['service'].get('name', _('<unknown>'))
+
+ service_method = port['service'].get('method', _('<none>'))
+
+ reference = self.__ports_store.append(None,
+ [port['id'],
+ port['protocol'],
+ port['state']['state'],
+ service_name,
+ service_method,
+ color,
+ True])
+
+ for key in port['state']:
+ self.__ports_store.append(reference,
+ [port['id'],
+ 'state',
+ key,
+ port['state'][key],
+ '',
+ 'white',
+ True])
+
+ for key in port['service']:
+
+ if key in ['servicefp']:
+
+ text = _('[%d] service: %s') % (port['id'], key)
+
+ self.__select_combobox.append_text(text)
+ self.__text.append(port['service'][key])
+
+ value = _('<special field>')
+
+ else:
+ value = port['service'][key]
+
+ self.__ports_store.append(reference,
+ [port['id'],
+ 'service',
+ key,
+ value,
+ '',
+ 'white',
+ True])
+
+ #for script in port['scripts']:
+ # text = _('[%d] script: %s') % (port['id'], script['id'])
+ # self.__select_combobox.append_text(text)
+ # self.__text.append(script['output'])
+ #
+ # self.__ports_store.append(reference,
+ # [port['id'],
+ # 'script',
+ # 'id',
+ # script['id'],
+ # _('<special field>'),
+ # 'white',
+ # True])
+
+ self.__ports_column = list()
+
+ for i in range(len(PORTS_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=PORTS_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__ports_column.append(column)
+
+ self.__ports_column[i].set_reorderable(True)
+ self.__ports_column[i].set_resizable(True)
+ self.__ports_column[i].set_sort_column_id(i)
+ self.__ports_column[i].set_attributes(self.__cell,
+ text=i,
+ background=5,
+ editable=6)
+
+ self.__ports_treeview.append_column(self.__ports_column[i])
+
+ self.__ports_scroll.add_with_viewport(self.__ports_treeview)
+
+ # extraports information
+ number_of_xports = 0
+
+ self.__xports_scroll = BWScrolledWindow()
+
+ self.__xports_store = Gtk.TreeStore.new([GObject.TYPE_INT,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_BOOLEAN])
+
+ self.__xports_treeview = Gtk.TreeView.new_with_model(self.__xports_store)
+
+ for xports in self.__node.get_info('extraports'):
+
+ color = get_service_color(xports['state'])
+ number_of_xports += xports['count']
+
+ reference = self.__xports_store.append(
+ None, [xports['count'], xports['state'],
+ ", ".join(xports['reason']), color, True])
+
+ for xreason in xports['all_reason']:
+ self.__xports_store.append(reference,
+ [xreason['count'],
+ xports['state'],
+ xreason['reason'],
+ 'white',
+ True])
+
+ self.__xports_column = list()
+
+ for i in range(len(EXTRAPORTS_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=EXTRAPORTS_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__xports_column.append(column)
+
+ self.__xports_column[i].set_reorderable(True)
+ self.__xports_column[i].set_resizable(True)
+ self.__xports_column[i].set_sort_column_id(i)
+ self.__xports_column[i].set_attributes(self.__cell,
+ text=i,
+ background=3,
+ editable=4)
+
+ self.__xports_treeview.append_column(self.__xports_column[i])
+
+ xports_label_text = _('Extraports (%s)') % number_of_xports
+ self.__xports_label = BWLabel(xports_label_text)
+
+ self.__xports_scroll.add_with_viewport(self.__xports_treeview)
+
+ self.append_page(self.__ports_scroll, self.__ports_label)
+ self.append_page(self.__xports_scroll, self.__xports_label)
+ self.append_page(self.__viewer, BWLabel(_('Special fields')))
+
+ if len(self.__text) > 0:
+ self.__select_combobox.set_active(0)
+
+ def __change_text_value(self, widget):
+ """
+ """
+ id = self.__select_combobox.get_active()
+
+ self.__texteditor.bw_set_text(self.__text[id])
+
+
+class SystemPage(BWScrolledWindow):
+ """
+ """
+ def __init__(self, node):
+ """
+ """
+ BWScrolledWindow.__init__(self)
+
+ self.__node = node
+ self.__font = Pango.FontDescription('Monospace')
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__vbox = BWVBox()
+ self.__vbox.set_border_width(6)
+
+ self.__cell = Gtk.CellRendererText()
+
+ self.__general_frame = BWExpander(_('General information'))
+ self.__sequences_frame = BWExpander(_('Sequences'))
+ self.__os_frame = BWExpander(_('Operating System'))
+
+ self.__sequences_frame.bw_add(Gtk.Label.new(_('No sequence information.')))
+ self.__os_frame.bw_add(Gtk.Label.new(_('No OS information.')))
+
+ # general information widgets
+ self.__general = BWTable(3, 2)
+
+ self.__address_label = BWSectionLabel(_('Address:'))
+ self.__address_list = Gtk.ComboBoxText.new_with_entry()
+ self.__address_list.get_child().set_editable(False)
+
+ for address in self.__node.get_info('addresses'):
+
+ params = address['type'], address['addr']
+ address_text = SYSTEM_ADDRESS_TEXT % params
+
+ if address['vendor'] is not None and address['vendor'] != '':
+ address_text += " (%s)" % address['vendor']
+
+ self.__address_list.append_text(address_text)
+
+ self.__address_list.set_active(0)
+
+ self.__general.bw_attach_next(self.__address_label,
+ yoptions=Gtk.AttachOptions.FILL,
+ xoptions=Gtk.AttachOptions.FILL)
+ self.__general.bw_attach_next(self.__address_list, yoptions=Gtk.AttachOptions.FILL)
+
+ if self.__node.get_info('hostnames') is not None:
+
+ self.__hostname_label = BWSectionLabel(_('Hostname:'))
+ self.__hostname_list = Gtk.ComboBoxText.new_with_entry()
+ self.__hostname_list.get_child().set_editable(False)
+
+ for hostname in self.__node.get_info('hostnames'):
+
+ params = hostname['type'], hostname['name']
+ self.__hostname_list.append_text(SYSTEM_ADDRESS_TEXT % params)
+
+ self.__hostname_list.set_active(0)
+
+ self.__general.bw_attach_next(self.__hostname_label,
+ yoptions=Gtk.AttachOptions.FILL,
+ xoptions=Gtk.AttachOptions.FILL)
+ self.__general.bw_attach_next(self.__hostname_list,
+ yoptions=Gtk.AttachOptions.FILL)
+
+ if self.__node.get_info('uptime') is not None:
+
+ self.__uptime_label = BWSectionLabel(_('Last boot:'))
+
+ seconds = self.__node.get_info('uptime')['seconds']
+ lastboot = self.__node.get_info('uptime')['lastboot']
+
+ text = _('%s (%s seconds).') % (lastboot, seconds)
+
+ self.__uptime_value = BWLabel(text)
+ self.__uptime_value.set_selectable(True)
+ self.__uptime_value.set_line_wrap(False)
+
+ self.__general.bw_attach_next(self.__uptime_label,
+ yoptions=Gtk.AttachOptions.FILL,
+ xoptions=Gtk.AttachOptions.FILL)
+ self.__general.bw_attach_next(self.__uptime_value,
+ yoptions=Gtk.AttachOptions.FILL)
+
+ self.__general_frame.bw_add(self.__general)
+ self.__general_frame.set_expanded(True)
+
+ sequences = self.__node.get_info('sequences')
+ if len(sequences) > 0:
+ self.__sequences_frame.bw_add(
+ self.__create_sequences_widget(sequences))
+
+ # operating system information widgets
+ self.__os = Gtk.Notebook()
+
+ os = self.__node.get_info('os')
+
+ if os is not None:
+
+ if 'matches' in os:
+
+ self.__match_scroll = BWScrolledWindow()
+
+ self.__match_store = Gtk.ListStore.new([str, str, int, bool])
+ self.__match_treeview = Gtk.TreeView.new_with_model(self.__match_store)
+
+ for os_match in os['matches']:
+
+ self.__match_store.append([os_match['accuracy'],
+ os_match['name'],
+ #os_match['db_line'],
+ 0, # unsupported
+ True])
+
+ self.__match_column = list()
+
+ for i in range(len(OSMATCH_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=OSMATCH_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__match_column.append(column)
+
+ self.__match_column[i].set_reorderable(True)
+ self.__match_column[i].set_resizable(True)
+ self.__match_column[i].set_attributes(self.__cell,
+ text=i,
+ editable=3)
+
+ self.__match_column[i].set_sort_column_id(i)
+ self.__match_treeview.append_column(self.__match_column[i])
+
+ self.__match_scroll.add_with_viewport(self.__match_treeview)
+
+ self.__os.append_page(self.__match_scroll, BWLabel(_('Match')))
+
+ if 'classes' in os:
+
+ self.__class_scroll = BWScrolledWindow()
+
+ self.__class_store = Gtk.ListStore.new([str, str, str, str, str, bool])
+ self.__class_treeview = Gtk.TreeView.new_with_model(self.__class_store)
+
+ for os_class in os['classes']:
+
+ os_gen = os_class.get('os_gen', '')
+
+ self.__class_store.append([os_class['accuracy'],
+ os_class['vendor'],
+ os_class['type'],
+ os_class['os_family'],
+ os_gen,
+ True])
+
+ self.__class_column = list()
+
+ for i in range(len(OSCLASS_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=OSCLASS_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__class_column.append(column)
+
+ self.__class_column[i].set_reorderable(True)
+ self.__class_column[i].set_resizable(True)
+ self.__class_column[i].set_attributes(self.__cell,
+ text=i,
+ editable=5)
+
+ self.__class_column[i].set_sort_column_id(i)
+ self.__class_treeview.append_column(self.__class_column[i])
+
+ self.__class_scroll.add_with_viewport(self.__class_treeview)
+
+ self.__os.append_page(self.__class_scroll, BWLabel(_('Class')))
+
+ self.__fp_viewer = BWTextEditor()
+ self.__fp_viewer.bw_modify_font(self.__font)
+ self.__fp_viewer.bw_set_editable(False)
+ self.__fp_viewer.bw_set_text(os['fingerprint'])
+
+ self.__fp_ports = BWHBox()
+ self.__fp_label = BWSectionLabel(_('Used ports:'))
+
+ self.__fp_ports_list = Gtk.ComboBoxText.new_with_entry()
+ self.__fp_ports_list.get_child().set_editable(False)
+
+ self.__fp_vbox = BWVBox()
+
+ if 'used_ports' in os:
+
+ used_ports = os['used_ports']
+
+ for port in used_ports:
+
+ params = port['id'], port['protocol'], port['state']
+ self.__fp_ports_list.append_text(USED_PORTS_TEXT % params)
+
+ self.__fp_ports_list.set_active(0)
+
+ self.__fp_ports.bw_pack_start_noexpand_nofill(self.__fp_label)
+ self.__fp_ports.bw_pack_start_expand_fill(self.__fp_ports_list)
+
+ self.__fp_vbox.bw_pack_start_noexpand_nofill(self.__fp_ports)
+
+ self.__os.append_page(self.__fp_viewer, BWLabel(_('Fingerprint')))
+ self.__fp_vbox.bw_pack_start_expand_fill(self.__os)
+
+ self.__os_frame.bw_add(self.__fp_vbox)
+ self.__os_frame.set_expanded(True)
+
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__general_frame)
+ self.__vbox.bw_pack_start_expand_fill(self.__os_frame)
+ self.__vbox.bw_pack_start_noexpand_nofill(self.__sequences_frame)
+
+ self.add_with_viewport(self.__vbox)
+
+ def __create_sequences_widget(self, sequences):
+ """Return a widget representing various OS detection sequences. The
+ sequences argument is a dict with zero or more of the keys 'tcp',
+ 'ip_id', and 'tcp_ts'."""
+ # sequences information widgets
+ table = BWTable(5, 3)
+
+ table.attach(BWSectionLabel(_('Class')), 1, 2, 0, 1)
+ table.attach(BWSectionLabel(_('Values')), 2, 3, 0, 1)
+
+ table.attach(BWSectionLabel('TCP *'), 0, 1, 1, 2)
+ table.attach(BWSectionLabel('IP ID'), 0, 1, 2, 3)
+ table.attach(BWSectionLabel(_('TCP Timestamp')), 0, 1, 3, 4)
+
+ tcp = sequences.get('tcp')
+ if tcp is not None:
+ tcp_class = BWLabel(tcp['class'])
+ tcp_class.set_selectable(True)
+
+ table.attach(tcp_class, 1, 2, 1, 2)
+
+ tcp_values = Gtk.ComboBoxText.new_with_entry()
+
+ for value in tcp['values']:
+ tcp_values.append_text(value)
+
+ tcp_values.set_active(0)
+
+ table.attach(tcp_values, 2, 3, 1, 2)
+
+ tcp_note = BWLabel()
+ tcp_note.set_selectable(True)
+ tcp_note.set_line_wrap(False)
+ tcp_note.set_alignment(1.0, 0.5)
+ tcp_note.set_markup(
+ TCP_SEQ_NOTE % (tcp['index'], tcp['difficulty']))
+
+ table.attach(tcp_note, 0, 3, 4, 5)
+
+ ip_id = sequences.get('ip_id')
+ if ip_id is not None:
+ ip_id_class = BWLabel(ip_id['class'])
+ ip_id_class.set_selectable(True)
+
+ table.attach(ip_id_class, 1, 2, 2, 3)
+
+ ip_id_values = Gtk.ComboBoxText.new_with_entry()
+
+ for value in ip_id['values']:
+ ip_id_values.append_text(value)
+
+ ip_id_values.set_active(0)
+
+ table.attach(ip_id_values, 2, 3, 2, 3)
+
+ tcp_ts = sequences.get('tcp_ts')
+ if tcp_ts is not None:
+ tcp_ts_class = BWLabel(tcp_ts['class'])
+ tcp_ts_class.set_selectable(True)
+
+ table.attach(tcp_ts_class, 1, 2, 3, 4)
+
+ if tcp_ts['values'] is not None:
+
+ tcp_ts_values = Gtk.ComboBoxText.new_with_entry()
+
+ for value in tcp_ts['values']:
+ tcp_ts_values.append_text(value)
+
+ tcp_ts_values.set_active(0)
+
+ table.attach(tcp_ts_values, 2, 3, 3, 4)
+
+ return table
+
+
+class TraceroutePage(BWVBox):
+ """
+ """
+ def __init__(self, node):
+ """
+ """
+ BWVBox.__init__(self)
+ self.set_border_width(6)
+
+ self.__node = node
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ trace = self.__node.get_info('trace')
+ hops = None
+ if trace is not None:
+ hops = trace.get("hops")
+ if hops is None or len(hops) == 0:
+
+ self.__trace_label = Gtk.Label.new(NO_TRACE_TEXT)
+ self.pack_start(self.__trace_label, True, True, 0)
+
+ else:
+
+ # add hops
+ hops = self.__node.get_info('trace')['hops']
+ ttls = [int(i['ttl']) for i in hops]
+
+ self.__cell = Gtk.CellRendererText()
+
+ self.__trace_scroll = BWScrolledWindow()
+ self.__trace_scroll.set_border_width(0)
+
+ self.__trace_store = Gtk.ListStore.new([int, str, str, str, str, bool])
+ self.__trace_treeview = Gtk.TreeView.new_with_model(self.__trace_store)
+
+ count = 0
+
+ for i in range(1, max(ttls) + 1):
+
+ if i in ttls:
+
+ hop = hops[count]
+ count += 1
+
+ self.__trace_store.append([hop['ttl'],
+ hop['rtt'],
+ hop['ip'],
+ hop['hostname'],
+ HOP_COLOR['known'],
+ True])
+
+ else:
+ self.__trace_store.append([i,
+ '',
+ _('<unknown>'),
+ '',
+ HOP_COLOR['unknown'],
+ True])
+
+ self.__trace_column = list()
+
+ for i in range(len(TRACE_HEADER)):
+
+ column = Gtk.TreeViewColumn(title=TRACE_HEADER[i],
+ cell_renderer=self.__cell,
+ text=i)
+
+ self.__trace_column.append(column)
+
+ self.__trace_column[i].set_reorderable(True)
+ self.__trace_column[i].set_resizable(True)
+ self.__trace_column[i].set_attributes(self.__cell,
+ text=i,
+ background=4,
+ editable=5)
+
+ self.__trace_treeview.append_column(self.__trace_column[i])
+
+ self.__trace_column[0].set_sort_column_id(0)
+
+ self.__trace_scroll.add_with_viewport(self.__trace_treeview)
+
+ self.__trace_info = (self.__node.get_info('trace')['port'],
+ self.__node.get_info('trace')['protocol'],
+ len(self.__node.get_info('trace')['hops']))
+
+ self.__trace_label = BWLabel(TRACE_TEXT % self.__trace_info)
+ self.__trace_label.set_use_markup(True)
+
+ self.bw_pack_start_expand_fill(self.__trace_scroll)
+ self.bw_pack_start_noexpand_nofill(self.__trace_label)
diff --git a/zenmap/radialnet/gui/NodeWindow.py b/zenmap/radialnet/gui/NodeWindow.py
new file mode 100644
index 0000000..7c38078
--- /dev/null
+++ b/zenmap/radialnet/gui/NodeWindow.py
@@ -0,0 +1,129 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, Gdk, Pango
+
+import radialnet.util.drawing as drawing
+
+from radialnet.bestwidgets.windows import BWWindow
+from radialnet.bestwidgets.boxes import BWVBox, BWHBox
+from radialnet.bestwidgets.labels import BWSectionLabel
+from radialnet.gui.Image import Application
+from radialnet.gui.NodeNotebook import NodeNotebook
+
+
+DIMENSION_NORMAL = (600, 400)
+
+
+class NodeWindow(BWWindow):
+ """
+ """
+ def __init__(self, node, position):
+ """
+ """
+ BWWindow.__init__(self, Gtk.WindowType.TOPLEVEL)
+ self.move(position[0], position[1])
+ self.set_default_size(DIMENSION_NORMAL[0], DIMENSION_NORMAL[1])
+
+ self.__node = node
+
+ self.__title_font = Pango.FontDescription('Monospace Bold')
+
+ self.__icon = Application()
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ self.__content = BWVBox()
+ self.__head = BWHBox(spacing=2)
+
+ self.__notebook = NodeNotebook(self.__node)
+
+ # create head elements
+
+ # icon with node's score color
+ self.__color_box = Gtk.EventBox()
+ self.__color_image = Gtk.Image()
+ self.__color_image.set_from_file(self.__icon.get_icon('border'))
+ self.__color_box.add(self.__color_image)
+ self.__color_box.set_size_request(15, 15)
+ r, g, b = drawing.cairo_to_gdk_color(
+ self.__node.get_draw_info('color'))
+ self.__color_box.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(r, g, b))
+
+ # title with the node ip and hostname
+ self.__title = self.__node.get_host().get_hostname()
+
+ self.set_title(self.__title)
+
+ self.__title_label = BWSectionLabel(self.__title)
+ self.__title_label.modify_font(self.__title_font)
+
+ # packing head elements
+ self.__head.bw_pack_start_noexpand_nofill(self.__color_box)
+ self.__head.bw_pack_start_expand_fill(self.__title_label)
+
+ # packing all to content
+ self.__content.bw_pack_start_noexpand_nofill(self.__head)
+ self.__content.bw_pack_start_expand_fill(self.__notebook)
+
+ # add content to window
+ self.add(self.__content)
diff --git a/zenmap/radialnet/gui/RadialNet.py b/zenmap/radialnet/gui/RadialNet.py
new file mode 100644
index 0000000..1a395dd
--- /dev/null
+++ b/zenmap/radialnet/gui/RadialNet.py
@@ -0,0 +1,2020 @@
+# vim: set encoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Gdk
+
+import math
+import cairo
+
+from functools import reduce
+
+import radialnet.util.geometry as geometry
+import radialnet.util.misc as misc
+
+from radialnet.core.Coordinate import PolarCoordinate, CartesianCoordinate
+from radialnet.core.Interpolation import Linear2DInterpolator
+from radialnet.core.Graph import Node
+from radialnet.gui.NodeWindow import NodeWindow
+from radialnet.gui.Image import Icons, get_pixels_for_cairo_image_surface
+
+REGION_COLORS = [(1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)]
+REGION_RED = 0
+REGION_YELLOW = 1
+REGION_GREEN = 2
+
+SQUARE_TYPES = ['router', 'switch', 'wap']
+
+ICON_DICT = {'router': 'router',
+ 'switch': 'switch',
+ 'wap': 'wireless',
+ 'firewall': 'firewall'}
+
+POINTER_JUMP_TO = 0
+POINTER_INFO = 1
+POINTER_GROUP = 2
+POINTER_FILL = 3
+
+LAYOUT_SYMMETRIC = 0
+LAYOUT_WEIGHTED = 1
+
+INTERPOLATION_CARTESIAN = 0
+INTERPOLATION_POLAR = 1
+
+FILE_TYPE_PDF = 1
+FILE_TYPE_PNG = 2
+FILE_TYPE_PS = 3
+FILE_TYPE_SVG = 4
+
+
+class RadialNet(Gtk.DrawingArea):
+ """
+ Radial network visualization widget
+ """
+ def __init__(self, layout=LAYOUT_SYMMETRIC):
+ """
+ Constructor method of RadialNet widget class
+ @type number_of_rings: number
+ @param number_of_rings: Number of rings in radial layout
+ """
+ self.__center_of_widget = (0, 0)
+ self.__graph = None
+
+ self.__number_of_rings = 0
+ self.__ring_gap = 30
+ self.__min_ring_gap = 10
+
+ self.__layout = layout
+ self.__interpolation = INTERPOLATION_POLAR
+ self.__interpolation_slow_in_out = True
+
+ self.__animating = False
+ self.__animation_rate = 1000 // 60 # 60Hz (human perception factor)
+ self.__number_of_frames = 60
+
+ self.__scale = 1.0
+ # rotated so that single-host traceroute doesn't have overlapping hosts
+ self.__rotate = 225
+ self.__translation = (0, 0)
+
+ self.__button1_press = False
+ self.__button2_press = False
+ self.__button3_press = False
+
+ self.__last_motion_point = None
+
+ self.__fisheye = False
+ self.__fisheye_ring = 0
+ self.__fisheye_spread = 0.5
+ self.__fisheye_interest = 2
+
+ self.__show_address = True
+ self.__show_hostname = True
+ self.__show_icon = True
+ self.__show_latency = False
+ self.__show_ring = True
+ self.__show_region = True
+ self.__region_color = REGION_RED
+
+ self.__node_views = dict()
+ self.__last_group_node = None
+
+ self.__pointer_status = POINTER_JUMP_TO
+
+ self.__sorted_nodes = list()
+
+ self.__icon = Icons()
+
+ super(RadialNet, self).__init__()
+
+ self.connect('draw', self.draw)
+ self.connect('button_press_event', self.button_press)
+ self.connect('button_release_event', self.button_release)
+ self.connect('motion_notify_event', self.motion_notify)
+ self.connect('enter_notify_event', self.enter_notify)
+ self.connect('leave_notify_event', self.leave_notify)
+ self.connect('key_press_event', self.key_press)
+ self.connect('key_release_event', self.key_release)
+ self.connect('scroll_event', self.scroll_event)
+
+ self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
+ Gdk.EventMask.ENTER_NOTIFY_MASK |
+ Gdk.EventMask.LEAVE_NOTIFY_MASK |
+ Gdk.EventMask.KEY_PRESS_MASK |
+ Gdk.EventMask.KEY_RELEASE_MASK |
+ Gdk.EventMask.POINTER_MOTION_HINT_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK |
+ Gdk.EventMask.SCROLL_MASK)
+
+ self.set_can_focus(True)
+ self.grab_focus()
+
+ def graph_is_not_empty(function):
+ """
+ Decorator function to prevent the execution when graph not is set
+ @type function: function
+ @param function: Protected function
+ """
+ def check_graph_status(*args):
+ if args[0].__graph is None:
+ return False
+ return function(*args)
+
+ return check_graph_status
+
+ def not_is_in_animation(function):
+ """
+ Decorator function to prevent the execution when graph is animating
+ @type function: function
+ @param function: Protected function
+ """
+ def check_animation_status(*args):
+ if args[0].__animating:
+ return False
+ return function(*args)
+
+ return check_animation_status
+
+ def save_drawing_to_file(self, file, type=FILE_TYPE_PNG):
+ """
+ """
+ allocation = self.get_allocation()
+
+ if type == FILE_TYPE_PDF:
+ self.surface = cairo.PDFSurface(file,
+ allocation.width,
+ allocation.height)
+ elif type == FILE_TYPE_PNG:
+ self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+ allocation.width,
+ allocation.height)
+ elif type == FILE_TYPE_PS:
+ self.surface = cairo.PSSurface(file,
+ allocation.width,
+ allocation.height)
+ elif type == FILE_TYPE_SVG:
+ self.surface = cairo.SVGSurface(file,
+ allocation.width,
+ allocation.height)
+ else:
+ raise TypeError('unknown surface type')
+
+ context = cairo.Context(self.surface)
+
+ context.rectangle(0, 0, allocation.width, allocation.height)
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill()
+
+ self.__draw(context)
+
+ if type == FILE_TYPE_PNG:
+ self.surface.write_to_png(file)
+
+ self.surface.flush()
+ self.surface.finish()
+
+ return True
+
+ def get_slow_inout(self):
+ """
+ """
+ return self.__interpolation_slow_in_out
+
+ def set_slow_inout(self, value):
+ """
+ """
+ self.__interpolation_slow_in_out = value
+
+ def get_region_color(self):
+ """
+ """
+ return self.__region_color
+
+ def set_region_color(self, value):
+ """
+ """
+ self.__region_color = value
+
+ def get_show_region(self):
+ """
+ """
+ return self.__show_region
+
+ def set_show_region(self, value):
+ """
+ """
+ self.__show_region = value
+ self.queue_draw()
+
+ def get_pointer_status(self):
+ """
+ """
+ return self.__pointer_status
+
+ def set_pointer_status(self, pointer_status):
+ """
+ """
+ self.__pointer_status = pointer_status
+
+ def get_show_address(self):
+ """
+ """
+ return self.__show_address
+
+ def get_show_hostname(self):
+ """
+ """
+ return self.__show_hostname
+
+ def get_show_ring(self):
+ """
+ """
+ return self.__show_ring
+
+ def set_show_address(self, value):
+ """
+ """
+ self.__show_address = value
+ self.queue_draw()
+
+ def set_show_hostname(self, value):
+ """
+ """
+ self.__show_hostname = value
+ self.queue_draw()
+
+ def set_show_ring(self, value):
+ """
+ """
+ self.__show_ring = value
+ self.queue_draw()
+
+ def get_min_ring_gap(self):
+ """
+ """
+ return self.__min_ring_gap
+
+ @graph_is_not_empty
+ @not_is_in_animation
+ def set_min_ring_gap(self, value):
+ """
+ """
+ self.__min_ring_gap = int(value)
+
+ if self.__ring_gap < self.__min_ring_gap:
+ self.__ring_gap = self.__min_ring_gap
+
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ return True
+
+ def get_number_of_frames(self):
+ """
+ """
+ return self.__number_of_frames
+
+ @not_is_in_animation
+ def set_number_of_frames(self, number_of_frames):
+ """
+ """
+ if number_of_frames > 2:
+
+ self.__number_of_frames = int(number_of_frames)
+ return True
+
+ self.__number_of_frames = 3
+ return False
+
+ @not_is_in_animation
+ def update_layout(self):
+ """
+ """
+ if self.__graph is None:
+ return
+ self.__animating = True
+ self.__calc_interpolation(self.__graph.get_main_node())
+ self.__livens_up()
+
+ @not_is_in_animation
+ def set_layout(self, layout):
+ """
+ """
+ if self.__layout != layout:
+
+ self.__layout = layout
+
+ if self.__graph is not None:
+
+ self.__animating = True
+ self.__calc_interpolation(self.__graph.get_main_node())
+ self.__livens_up()
+
+ return True
+
+ return False
+
+ def get_layout(self):
+ """
+ """
+ return self.__layout
+
+ @not_is_in_animation
+ def set_interpolation(self, interpolation):
+ """
+ """
+ self.__interpolation = interpolation
+
+ return True
+
+ def get_interpolation(self):
+ """
+ """
+ return self.__interpolation
+
+ def get_number_of_rings(self):
+ """
+ """
+ return self.__number_of_rings
+
+ def get_fisheye_ring(self):
+ """
+ """
+ return self.__fisheye_ring
+
+ def get_fisheye_interest(self):
+ """
+ """
+ return self.__fisheye_interest
+
+ def get_fisheye_spread(self):
+ """
+ """
+ return self.__fisheye_spread
+
+ def get_fisheye(self):
+ """
+ """
+ return self.__fisheye
+
+ def set_fisheye(self, enable):
+ """
+ """
+ self.__fisheye = enable
+
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ def set_fisheye_ring(self, value):
+ """
+ """
+ self.__fisheye_ring = value
+ self.__check_fisheye_ring()
+
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ def set_fisheye_interest(self, value):
+ """
+ """
+ self.__fisheye_interest = value
+
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ def set_fisheye_spread(self, value):
+ """
+ """
+ self.__fisheye_spread = value
+
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ def get_show_icon(self):
+ """
+ """
+ return self.__show_icon
+
+ def set_show_icon(self, value):
+ """
+ """
+ self.__show_icon = value
+ self.queue_draw()
+
+ def get_show_latency(self):
+ """
+ """
+ return self.__show_latency
+
+ def set_show_latency(self, value):
+ """
+ """
+ self.__show_latency = value
+ self.queue_draw()
+
+ def get_scale(self):
+ """
+ """
+ return self.__scale
+
+ def get_zoom(self):
+ """
+ """
+ return int(round(self.__scale * 100))
+
+ def set_scale(self, scale):
+ """
+ """
+ if scale >= 0.01:
+
+ self.__scale = scale
+ self.queue_draw()
+
+ def set_zoom(self, zoom):
+ """
+ """
+ if float(zoom) >= 1:
+
+ self.set_scale(float(zoom) / 100.0)
+ self.queue_draw()
+
+ def get_ring_gap(self):
+ """
+ """
+ return self.__ring_gap
+
+ @not_is_in_animation
+ def set_ring_gap(self, ring_gap):
+ """
+ """
+ if ring_gap >= self.__min_ring_gap:
+
+ self.__ring_gap = ring_gap
+ self.__update_nodes_positions()
+ self.queue_draw()
+
+ def scroll_event(self, widget, event):
+ """
+ """
+ if event.direction == Gdk.ScrollDirection.UP:
+ self.set_scale(self.__scale + 0.01)
+
+ if event.direction == Gdk.ScrollDirection.DOWN:
+ self.set_scale(self.__scale - 0.01)
+
+ self.queue_draw()
+
+ @graph_is_not_empty
+ @not_is_in_animation
+ def key_press(self, widget, event):
+ """
+ """
+ key = Gdk.keyval_name(event.keyval)
+
+ if key == 'KP_Add':
+ self.set_ring_gap(self.__ring_gap + 1)
+
+ elif key == 'KP_Subtract':
+ self.set_ring_gap(self.__ring_gap - 1)
+
+ elif key == 'Page_Up':
+ self.set_scale(self.__scale + 0.01)
+
+ elif key == 'Page_Down':
+ self.set_scale(self.__scale - 0.01)
+
+ self.queue_draw()
+
+ return True
+
+ @graph_is_not_empty
+ def key_release(self, widget, event):
+ """
+ """
+ key = Gdk.keyval_name(event.keyval)
+
+ if key == 'c':
+ self.__translation = (0, 0)
+
+ elif key == 'r':
+ self.__show_ring = not self.__show_ring
+
+ elif key == 'a':
+ self.__show_address = not self.__show_address
+
+ elif key == 'h':
+ self.__show_hostname = not self.__show_hostname
+
+ elif key == 'i':
+ self.__show_icon = not self.__show_icon
+
+ elif key == 'l':
+ self.__show_latency = not self.__show_latency
+
+ self.queue_draw()
+
+ return True
+
+ @graph_is_not_empty
+ @not_is_in_animation
+ def enter_notify(self, widget, event):
+ """
+ """
+ self.grab_focus()
+ return False
+
+ @graph_is_not_empty
+ @not_is_in_animation
+ def leave_notify(self, widget, event):
+ """
+ """
+ for node in self.__graph.get_nodes():
+ node.set_draw_info({'over': False})
+
+ self.queue_draw()
+
+ return False
+
+ @graph_is_not_empty
+ def button_press(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ result = self.__get_node_by_coordinate(self.get_pointer())
+
+ if event.button == 1:
+ self.__button1_press = True
+
+ # animate if node is pressed
+ if self.__pointer_status == POINTER_JUMP_TO and event.button == 1:
+
+ # prevent double animation
+ if self.__animating:
+ return False
+
+ if result is not None:
+
+ node, point = result
+ main_node = self.__graph.get_main_node()
+
+ if node != main_node:
+
+ if node.get_draw_info('group'):
+
+ node.set_draw_info({'group': False})
+ node.set_subtree_info({'grouped': False,
+ 'group_node': None})
+
+ self.__animating = True
+ self.__calc_interpolation(node)
+ self.__livens_up()
+
+ # group node if it's pressed
+ elif self.__pointer_status == POINTER_GROUP and event.button == 1:
+
+ # prevent group on animation
+ if self.__animating:
+ return False
+
+ if result is not None:
+
+ node, point = result
+ main_node = self.__graph.get_main_node()
+
+ if node != main_node:
+
+ if node.get_draw_info('group'):
+
+ node.set_draw_info({'group': False})
+ node.set_subtree_info({'grouped': False,
+ 'group_node': None})
+
+ else:
+
+ self.__last_group_node = node
+
+ node.set_draw_info({'group': True})
+ node.set_subtree_info({'grouped': True,
+ 'group_node': node})
+
+ self.__animating = True
+ self.__calc_interpolation(self.__graph.get_main_node())
+ self.__livens_up()
+
+ # setting to show node's region
+ elif self.__pointer_status == POINTER_FILL and event.button == 1:
+
+ if result is not None:
+
+ node, point = result
+
+ if node.get_draw_info('region') == self.__region_color:
+ node.set_draw_info({'region': None})
+
+ else:
+ node.set_draw_info({'region': self.__region_color})
+
+ self.queue_draw()
+
+ # show node details
+ elif event.button == 3 or self.__pointer_status == POINTER_INFO:
+
+ if event.button == 3:
+ self.__button3_press = True
+
+ if result is not None:
+
+ # first returned value is not meaningful and should be ignored
+ _, xw, yw = self.get_window().get_origin()
+ node, point = result
+ x, y = point
+
+ if node in self.__node_views.keys():
+
+ self.__node_views[node].present()
+
+ elif node.get_draw_info('scanned'):
+
+ view = NodeWindow(node, (int(xw + x), int(yw + y)))
+
+ def close_view(view, event, node):
+ view.destroy()
+ del self.__node_views[node]
+
+ view.connect("delete-event", close_view, node)
+ view.show_all()
+ self.__node_views[node] = view
+
+ return False
+
+ @graph_is_not_empty
+ def button_release(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ if event.button == 1:
+ self.__button1_press = False
+
+ if event.button == 2:
+ self.__button2_press = False
+
+ if event.button == 3:
+ self.__button3_press = False
+
+ self.grab_focus()
+
+ return False
+
+ @graph_is_not_empty
+ def motion_notify(self, widget, event):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type event: GtkEvent
+ @param event: Gtk event of widget
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ pointer = self.get_pointer()
+
+ for node in self.__graph.get_nodes():
+ node.set_draw_info({'over': False})
+
+ result = self.__get_node_by_coordinate(self.get_pointer())
+
+ if result is not None:
+ result[0].set_draw_info({'over': True})
+
+ elif self.__button1_press and self.__last_motion_point is not None:
+
+ ax, ay = pointer
+ ox, oy = self.__last_motion_point
+ tx, ty = self.__translation
+
+ self.__translation = (tx + ax - ox, ty - ay + oy)
+
+ self.__last_motion_point = pointer
+
+ self.grab_focus()
+ self.queue_draw()
+
+ return False
+
+ def draw(self, widget, context):
+ """
+ Drawing callback
+ @type widget: GtkWidget
+ @param widget: Gtk widget superclass
+ @type context: cairo.Context
+ @param context: cairo context class
+ @rtype: boolean
+ @return: Indicator of the event propagation
+ """
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill()
+
+ self.__draw(context)
+
+ return False
+
+ @graph_is_not_empty
+ def __draw(self, context):
+ """
+ Drawing method
+ """
+ # getting allocation reference
+ allocation = self.get_allocation()
+
+ self.__center_of_widget = (allocation.width // 2,
+ allocation.height // 2)
+
+ xc, yc = self.__center_of_widget
+
+ ax, ay = self.__translation
+
+ # xc = 320 yc = 240
+
+ # -1.5 | -0.5 ( 480, 360)
+ # -1.0 | 0.0 ( 320, 240)
+ # -0.5 | 0.5 ( 160, 120)
+ # 0.0 | 1.0 ( 0, 0)
+ # 0.5 | 1.5 (-160, -120)
+ # 1.0 | 2.0 (-320, -240)
+ # 1.5 | 2.5 (-480, -360)
+
+ # scaling and translate
+ factor = -(self.__scale - 1)
+
+ context.translate(xc * factor + ax, yc * factor - ay)
+
+ if self.__scale != 1.0:
+ context.scale(self.__scale, self.__scale)
+
+ # drawing over node's region
+ if self.__show_region and not self.__animating:
+
+ for node in self.__sorted_nodes:
+
+ not_grouped = not node.get_draw_info('grouped')
+
+ if node.get_draw_info('region') is not None and not_grouped:
+
+ xc, yc = self.__center_of_widget
+ r, g, b = REGION_COLORS[node.get_draw_info('region')]
+
+ start, final = node.get_draw_info('range')
+
+ i_radius = node.get_coordinate_radius()
+ f_radius = self.__calc_radius(self.__number_of_rings - 1)
+
+ is_fill_all = abs(final - start) == 360
+
+ final = math.radians(final + self.__rotate)
+ start = math.radians(start + self.__rotate)
+
+ context.move_to(xc, yc)
+ context.set_source_rgba(r, g, b, 0.1)
+ context.new_path()
+ context.arc(xc, yc, i_radius, -final, -start)
+ context.arc_negative(xc, yc, f_radius, -start, -final)
+ context.close_path()
+ context.fill()
+ context.stroke()
+
+ if not is_fill_all:
+
+ context.set_source_rgb(r, g, b)
+ context.set_line_width(1)
+
+ xa, ya = PolarCoordinate(
+ i_radius, final).to_cartesian()
+ xb, yb = PolarCoordinate(
+ f_radius, final).to_cartesian()
+
+ context.move_to(xc + xa, yc - ya)
+ context.line_to(xc + xb, yc - yb)
+ context.stroke()
+
+ xa, ya = PolarCoordinate(
+ i_radius, start).to_cartesian()
+ xb, yb = PolarCoordinate(
+ f_radius, start).to_cartesian()
+
+ context.move_to(xc + xa, yc - ya)
+ context.line_to(xc + xb, yc - yb)
+ context.stroke()
+
+ # drawing network rings
+ if self.__show_ring and not self.__animating:
+
+ for i in range(1, self.__number_of_rings):
+
+ radius = self.__calc_radius(i)
+
+ context.arc(xc, yc, radius, 0, 2 * math.pi)
+ context.set_source_rgb(0.8, 0.8, 0.8)
+ context.set_line_width(1)
+ context.stroke()
+
+ # drawing nodes and your connections
+ for edge in self.__graph.get_edges():
+
+ # check group constraints for edges
+ a, b = edge.get_nodes()
+
+ a_is_grouped = a.get_draw_info('grouped')
+ b_is_grouped = b.get_draw_info('grouped')
+
+ a_is_group = a.get_draw_info('group')
+ b_is_group = b.get_draw_info('group')
+
+ a_group = a.get_draw_info('group_node')
+ b_group = b.get_draw_info('group_node')
+
+ a_is_child = a in b.get_draw_info('children')
+ b_is_child = b in a.get_draw_info('children')
+
+ last_group = self.__last_group_node
+ groups = [a_group, b_group]
+
+ if last_group in groups and last_group is not None:
+ self.__draw_edge(context, edge)
+
+ elif not a_is_grouped or not b_is_grouped:
+
+ if not (a_is_group and b_is_child or
+ b_is_group and a_is_child):
+ self.__draw_edge(context, edge)
+
+ elif a_group != b_group:
+ self.__draw_edge(context, edge)
+
+ for node in reversed(self.__sorted_nodes):
+
+ # check group constraints for nodes
+ group = node.get_draw_info('group_node')
+ grouped = node.get_draw_info('grouped')
+
+ if group == self.__last_group_node or not grouped:
+ self.__draw_node(context, node)
+
+ def __draw_edge(self, context, edge):
+ """
+ Draw the connection between two nodes
+ @type : Edge
+ @param : The second node that will be connected
+ """
+ a, b = edge.get_nodes()
+
+ xa, ya = a.get_cartesian_coordinate()
+ xb, yb = b.get_cartesian_coordinate()
+ xc, yc = self.__center_of_widget
+
+ a_children = a.get_draw_info('children')
+ b_children = b.get_draw_info('children')
+
+ latency = edge.get_weights_mean()
+
+ # check if isn't an hierarchy connection
+ if a not in b_children and b not in a_children:
+ context.set_source_rgba(1.0, 0.6, 0.1, 0.8)
+
+ elif a.get_draw_info('no_route') or b.get_draw_info('no_route'):
+ context.set_source_rgba(0.0, 0.0, 0.0, 0.8)
+
+ else:
+ context.set_source_rgba(0.1, 0.5, 1.0, 0.8)
+
+ # calculating line thickness by latency
+ if latency is not None:
+
+ min = self.__graph.get_min_edge_mean_weight()
+ max = self.__graph.get_max_edge_mean_weight()
+
+ if max != min:
+ thickness = (latency - min) * 4 / (max - min) + 1
+
+ else:
+ thickness = 1
+
+ context.set_line_width(thickness)
+
+ else:
+
+ context.set_dash([2, 2])
+ context.set_line_width(1)
+
+ context.move_to(xc + xa, yc - ya)
+ context.line_to(xc + xb, yc - yb)
+ context.stroke()
+
+ context.set_dash([1, 0])
+
+ if not self.__animating and self.__show_latency:
+
+ if latency is not None:
+
+ context.set_font_size(8)
+ context.set_line_width(1)
+ context.move_to(xc + (xa + xb) / 2 + 1,
+ yc - (ya + yb) / 2 + 4)
+ context.show_text(str(round(latency, 2)))
+ context.stroke()
+
+ def __draw_node(self, context, node):
+ """
+ Draw nodes and your information
+ @type : NetNode
+ @param : The node to be drawn
+ """
+ x, y = node.get_cartesian_coordinate()
+ xc, yc = self.__center_of_widget
+ r, g, b = node.get_draw_info('color')
+ radius = node.get_draw_info('radius')
+
+ type = node.get_info('device_type')
+
+ x_gap = radius + 2
+ y_gap = 0
+
+ # draw group indication
+ if node.get_draw_info('group'):
+
+ x_gap += 5
+
+ if type in SQUARE_TYPES:
+ context.rectangle(xc + x - radius - 5,
+ yc - y - radius - 5,
+ 2 * radius + 10,
+ 2 * radius + 10)
+
+ else:
+ context.arc(xc + x, yc - y, radius + 5, 0, 2 * math.pi)
+
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ context.fill_preserve()
+
+ if node.deep_search_child(self.__graph.get_main_node()):
+ context.set_source_rgb(0.0, 0.0, 0.0)
+
+ else:
+ context.set_source_rgb(0.1, 0.5, 1.0)
+
+ context.set_line_width(2)
+ context.stroke()
+
+ # draw over node
+ if node.get_draw_info('over'):
+
+ context.set_line_width(0)
+
+ if type in SQUARE_TYPES:
+ context.rectangle(xc + x - radius - 5,
+ yc - y - radius - 5,
+ 2 * radius + 10,
+ 2 * radius + 10)
+
+ else:
+ context.arc(xc + x, yc - y, radius + 5, 0, 2 * math.pi)
+
+ context.set_source_rgb(0.1, 0.5, 1.0)
+ context.fill_preserve()
+ context.stroke()
+
+ # draw node
+ if type in SQUARE_TYPES:
+ context.rectangle(xc + x - radius,
+ yc - y - radius,
+ 2 * radius,
+ 2 * radius)
+
+ else:
+ context.arc(xc + x, yc - y, radius, 0, 2 * math.pi)
+
+ # draw icons
+ if not self.__animating and self.__show_icon:
+
+ icons = list()
+
+ if type in ICON_DICT.keys():
+ icons.append(self.__icon.get_pixbuf(ICON_DICT[type]))
+
+ if node.get_info('filtered'):
+ icons.append(self.__icon.get_pixbuf('padlock'))
+
+ for icon in icons:
+
+ stride, data = get_pixels_for_cairo_image_surface(icon)
+
+ # Cairo documentation says that the correct way to obtain a
+ # legal stride value is using the function
+ # cairo.ImageSurface.format_stride_for_width().
+ # But this method is only available since cairo 1.6. So we are
+ # using the stride returned by
+ # get_pixels_for_cairo_image_surface() function.
+ surface = cairo.ImageSurface.create_for_data(data,
+ cairo.FORMAT_ARGB32,
+ icon.get_width(),
+ icon.get_height(),
+ stride)
+
+ context.set_source_surface(surface,
+ round(xc + x + x_gap),
+ round(yc - y + y_gap - 6))
+ context.paint()
+
+ x_gap += 13
+
+ # draw node text
+ context.set_source_rgb(r, g, b)
+ context.fill_preserve()
+
+ if node.get_draw_info('valid'):
+ context.set_source_rgb(0.0, 0.0, 0.0)
+
+ else:
+ context.set_source_rgb(0.1, 0.5, 1.0)
+
+ if not self.__animating and self.__show_address:
+
+ context.set_font_size(8)
+ context.move_to(round(xc + x + x_gap),
+ round(yc - y + y_gap + 4))
+
+ hostname = node.get_info('hostname')
+
+ if hostname is not None and self.__show_hostname:
+ context.show_text(hostname)
+
+ elif node.get_info('ip') is not None:
+ context.show_text(node.get_info('ip'))
+
+ context.set_line_width(1)
+ context.stroke()
+
+ def __check_fisheye_ring(self):
+ """
+ """
+ if self.__fisheye_ring >= self.__number_of_rings:
+ self.__fisheye_ring = self.__number_of_rings - 1
+
+ def __set_number_of_rings(self, value):
+ """
+ """
+ self.__number_of_rings = value
+ self.__check_fisheye_ring()
+
+ def __fisheye_function(self, ring):
+ """
+ """
+ distance = abs(self.__fisheye_ring - ring)
+ level_of_detail = self.__ring_gap * self.__fisheye_interest
+ spread_distance = distance - distance * self.__fisheye_spread
+
+ value = level_of_detail / (spread_distance + 1)
+
+ if value < self.__min_ring_gap:
+ value = self.__min_ring_gap
+
+ return value
+
+ @graph_is_not_empty
+ @not_is_in_animation
+ def __update_nodes_positions(self):
+ """
+ """
+ for node in self.__sorted_nodes:
+
+ if node.get_draw_info('grouped'):
+
+ # deep group check
+ group = node.get_draw_info('group_node')
+
+ while group.get_draw_info('group_node') is not None:
+ group = group.get_draw_info('group_node')
+
+ ring = group.get_draw_info('ring')
+ node.set_coordinate_radius(self.__calc_radius(ring))
+
+ else:
+ ring = node.get_draw_info('ring')
+ node.set_coordinate_radius(self.__calc_radius(ring))
+
+ @graph_is_not_empty
+ def __get_node_by_coordinate(self, point):
+ """
+ """
+ xc, yc = self.__center_of_widget
+
+ for node in self.__graph.get_nodes():
+
+ if node.get_draw_info('grouped'):
+ continue
+
+ ax, ay = self.__translation
+
+ xn, yn = node.get_cartesian_coordinate()
+ center = (xc + xn * self.__scale + ax, yc - yn * self.__scale - ay)
+ radius = node.get_draw_info('radius') * self.__scale
+
+ type = node.get_info('device_type')
+
+ if type in SQUARE_TYPES:
+ if geometry.is_in_square(point, radius, center):
+ return node, center
+
+ else:
+ if geometry.is_in_circle(point, radius, center):
+ return node, center
+
+ return None
+
+ def __calc_radius(self, ring):
+ """
+ """
+ if self.__fisheye:
+
+ radius = 0
+
+ while ring > 0:
+
+ radius += self.__fisheye_function(ring)
+ ring -= 1
+
+ else:
+ radius = ring * self.__ring_gap
+
+ return radius
+
+ @graph_is_not_empty
+ def __arrange_nodes(self):
+ """
+ """
+ new_nodes = set([self.__graph.get_main_node()])
+ old_nodes = set()
+
+ number_of_needed_rings = 1
+ ring = 0
+
+ # while new nodes were found
+ while len(new_nodes) > 0:
+
+ tmp_nodes = set()
+
+ # for each new nodes
+ for node in new_nodes:
+
+ old_nodes.add(node)
+
+ # set ring location
+ node.set_draw_info({'ring': ring})
+
+ # check group constraints
+ if (node.get_draw_info('group') or
+ node.get_draw_info('grouped')):
+ children = node.get_draw_info('children')
+
+ else:
+
+ # getting connections and fixing multiple fathers
+ children = set()
+ for child in self.__graph.get_node_connections(node):
+ if child in old_nodes or child in new_nodes:
+ continue
+ if child.get_draw_info('grouped'):
+ continue
+ children.add(child)
+
+ # setting father foreign
+ for child in children:
+ child.set_draw_info({'father': node})
+
+ node.set_draw_info(
+ {'children': misc.sort_children(children, node)})
+ tmp_nodes.update(children)
+
+ # check group influence in number of rings
+ for node in tmp_nodes:
+
+ if not node.get_draw_info('grouped'):
+
+ number_of_needed_rings += 1
+ break
+
+ # update new nodes set
+ new_nodes.update(tmp_nodes)
+ new_nodes.difference_update(old_nodes)
+
+ ring += 1
+
+ self.__set_number_of_rings(number_of_needed_rings)
+
+ def __weighted_layout(self):
+ """
+ """
+ # calculating the space needed by each node
+ self.__graph.get_main_node().set_draw_info({'range': (0, 360)})
+ new_nodes = set([self.__graph.get_main_node()])
+
+ self.__graph.get_main_node().calc_needed_space()
+
+ while len(new_nodes) > 0:
+
+ node = new_nodes.pop()
+
+ # add only no grouped nodes
+ children = set()
+ for child in node.get_draw_info('children'):
+
+ if not child.get_draw_info('grouped'):
+ children.add(child)
+ new_nodes.add(child)
+
+ if len(children) > 0:
+
+ min, max = node.get_draw_info('range')
+
+ node_total = max - min
+ children_need = node.get_draw_info('children_need')
+
+ for child in children:
+
+ child_need = child.get_draw_info('space_need')
+ child_total = node_total * child_need / children_need
+
+ theta = child_total / 2 + min + self.__rotate
+
+ child.set_coordinate_theta(theta)
+ child.set_draw_info({'range': (min, min + child_total)})
+
+ min += child_total
+
+ def __symmetric_layout(self):
+ """
+ """
+ self.__graph.get_main_node().set_draw_info({'range': (0, 360)})
+ new_nodes = set([self.__graph.get_main_node()])
+
+ while len(new_nodes) > 0:
+
+ node = new_nodes.pop()
+
+ # add only no grouped nodes
+ children = set()
+ for child in node.get_draw_info('children'):
+
+ if not child.get_draw_info('grouped'):
+ children.add(child)
+ new_nodes.add(child)
+
+ if len(children) > 0:
+
+ min, max = node.get_draw_info('range')
+ factor = float(max - min) / len(children)
+
+ for child in children:
+
+ theta = factor / 2 + min + self.__rotate
+
+ child.set_coordinate_theta(theta)
+ child.set_draw_info({'range': (min, min + factor)})
+
+ min += factor
+
+ @graph_is_not_empty
+ def __calc_layout(self, reference):
+ """
+ """
+ # selecting layout algorithm
+ if self.__layout == LAYOUT_SYMMETRIC:
+ self.__symmetric_layout()
+
+ elif self.__layout == LAYOUT_WEIGHTED:
+ self.__weighted_layout()
+
+ # rotating focus' children to keep orientation
+ if reference is not None:
+
+ father, angle = reference
+ theta = father.get_coordinate_theta()
+ factor = theta - angle
+
+ for node in self.__graph.get_nodes():
+
+ theta = node.get_coordinate_theta()
+ node.set_coordinate_theta(theta - factor)
+
+ a, b = node.get_draw_info('range')
+ node.set_draw_info({'range': (a - factor, b - factor)})
+
+ @graph_is_not_empty
+ def __calc_node_positions(self, reference=None):
+ """
+ """
+ # set nodes' hierarchy
+ self.__arrange_nodes()
+ self.calc_sorted_nodes()
+
+ # set nodes' coordinate radius
+ for node in self.__graph.get_nodes():
+
+ ring = node.get_draw_info('ring')
+ node.set_coordinate_radius(self.__calc_radius(ring))
+
+ # set nodes' coordinate theta
+ self.__calc_layout(reference)
+
+ def __calc_interpolation(self, focus):
+ """
+ """
+ old_main_node = self.__graph.get_main_node()
+ self.__graph.set_main_node(focus)
+
+ # getting initial coordinates
+ for node in self.__graph.get_nodes():
+
+ if self.__interpolation == INTERPOLATION_POLAR:
+ coordinate = node.get_polar_coordinate()
+
+ elif self.__interpolation == INTERPOLATION_CARTESIAN:
+ coordinate = node.get_cartesian_coordinate()
+
+ node.set_draw_info({'start_coordinate': coordinate})
+
+ father = focus.get_draw_info('father')
+
+ # calculate nodes positions (and father orientation)?
+ if father is not None:
+
+ xa, ya = father.get_cartesian_coordinate()
+ xb, yb = focus.get_cartesian_coordinate()
+
+ angle = math.atan2(yb - ya, xb - xa)
+ angle = math.degrees(angle)
+
+ self.__calc_node_positions((father, 180 + angle))
+
+ else:
+ self.__calc_node_positions()
+
+ # steps for slow-in/slow-out animation
+ steps = list(range(self.__number_of_frames))
+
+ for i in range(len(steps) // 2):
+ steps[self.__number_of_frames - 1 - i] = steps[i]
+
+ # normalize angles and calculate interpolated points
+ for node in self.__sorted_nodes:
+
+ l2di = Linear2DInterpolator()
+
+ # change grouped nodes coordinate
+ if node.get_draw_info('grouped'):
+
+ group_node = node.get_draw_info('group_node')
+ a, b = group_node.get_draw_info('final_coordinate')
+
+ if self.__interpolation == INTERPOLATION_POLAR:
+ node.set_polar_coordinate(a, b)
+
+ elif self.__interpolation == INTERPOLATION_CARTESIAN:
+ node.set_cartesian_coordinate(a, b)
+
+ # change interpolation method
+ if self.__interpolation == INTERPOLATION_POLAR:
+
+ coordinate = node.get_polar_coordinate()
+ node.set_draw_info({'final_coordinate': coordinate})
+
+ # adjusting polar coordinates
+ ri, ti = node.get_draw_info('start_coordinate')
+ rf, tf = node.get_draw_info('final_coordinate')
+
+ # normalization [0, 360]
+ ti = geometry.normalize_angle(ti)
+ tf = geometry.normalize_angle(tf)
+
+ # against longest path
+ ti, tf = geometry.calculate_short_path(ti, tf)
+
+ # main node goes direct to center (no arc)
+ if node == self.__graph.get_main_node():
+ tf = ti
+
+ # old main node goes direct to new position (no arc)
+ if node == old_main_node:
+ ti = tf
+
+ node.set_draw_info({'start_coordinate': (ri, ti)})
+ node.set_draw_info({'final_coordinate': (rf, tf)})
+
+ elif self.__interpolation == INTERPOLATION_CARTESIAN:
+
+ coordinate = node.get_cartesian_coordinate()
+ node.set_draw_info({'final_coordinate': coordinate})
+
+ # calculate interpolated points
+ ai, bi = node.get_draw_info('start_coordinate')
+ af, bf = node.get_draw_info('final_coordinate')
+
+ l2di.set_start_point(ai, bi)
+ l2di.set_final_point(af, bf)
+
+ if self.__interpolation_slow_in_out:
+ points = l2di.get_weighed_points(
+ self.__number_of_frames, steps)
+
+ else:
+ points = l2di.get_points(self.__number_of_frames)
+
+ node.set_draw_info({'interpolated_coordinate': points})
+
+ return True
+
+ def __livens_up(self, index=0):
+ """
+ """
+ if self.__graph is None:
+ # Bail out if the graph became empty during an animation.
+ self.__last_group_node = None
+ self.__animating = False
+ return False
+
+ # prepare interpolated points
+ if index == 0:
+
+ # prevent unnecessary animation
+ no_need_to_move = True
+
+ for node in self.__graph.get_nodes():
+
+ ai, bi = node.get_draw_info('start_coordinate')
+ af, bf = node.get_draw_info('final_coordinate')
+
+ start_c = round(ai), round(bi)
+ final_c = round(af), round(bf)
+
+ if start_c != final_c:
+ no_need_to_move = False
+
+ if no_need_to_move:
+
+ self.__animating = False
+ return False
+
+ # move all nodes for pass 'index'
+ for node in self.__graph.get_nodes():
+
+ a, b = node.get_draw_info('interpolated_coordinate')[index]
+
+ if self.__interpolation == INTERPOLATION_POLAR:
+ node.set_polar_coordinate(a, b)
+
+ elif self.__interpolation == INTERPOLATION_CARTESIAN:
+ node.set_cartesian_coordinate(a, b)
+
+ self.queue_draw()
+
+ # animation continue condition
+ if index < self.__number_of_frames - 1:
+ GLib.timeout_add(self.__animation_rate, # time to recall
+ self.__livens_up, # recursive call
+ index + 1) # next iteration
+ else:
+ self.__last_group_node = None
+ self.__animating = False
+
+ return False
+
+ @not_is_in_animation
+ def set_graph(self, graph):
+ """
+ Set graph to be displayed in layout
+ @type : Graph
+ @param : Set the graph used in visualization
+ """
+ if graph.get_number_of_nodes() > 0:
+
+ self.__graph = graph
+
+ self.__calc_node_positions()
+ self.queue_draw()
+
+ else:
+ self.__graph = None
+
+ def get_scanned_nodes(self):
+ """
+ """
+ nodes = list()
+ if self.__graph is None:
+ return nodes
+
+ for node in self.__graph.get_nodes():
+
+ if node.get_draw_info('scanned'):
+ nodes.append(node)
+
+ return nodes
+
+ def get_graph(self):
+ """
+ """
+ return self.__graph
+
+ def set_empty(self):
+ """
+ """
+ del(self.__graph)
+ self.__graph = None
+
+ self.queue_draw()
+
+ def get_rotation(self):
+ """
+ """
+ return self.__rotate
+
+ @graph_is_not_empty
+ def set_rotation(self, angle):
+ """
+ """
+ delta = angle - self.__rotate
+ self.__rotate = angle
+
+ for node in self.__graph.get_nodes():
+
+ theta = node.get_coordinate_theta()
+ node.set_coordinate_theta(theta + delta)
+
+ self.queue_draw()
+
+ def get_translation(self):
+ """
+ """
+ return self.__translation
+
+ @graph_is_not_empty
+ def set_translation(self, translation):
+ """
+ """
+ self.__translation = translation
+ self.queue_draw()
+
+ def is_empty(self):
+ """
+ """
+ return self.__graph is None
+
+ def is_in_animation(self):
+ """
+ """
+ return self.__animating
+
+ def calc_sorted_nodes(self):
+ """
+ """
+ self.__sorted_nodes = list(self.__graph.get_nodes())
+ self.__sorted_nodes.sort(key=lambda n: n.get_draw_info('ring'))
+
+
+class NetNode(Node):
+ """
+ Node class for radial network widget
+ """
+ def __init__(self):
+ """
+ """
+ self.__draw_info = dict()
+ """Hash with draw information"""
+ self.__coordinate = PolarCoordinate()
+
+ super(NetNode, self).__init__()
+
+ def get_host(self):
+ """
+ Set the HostInfo that this node represents
+ """
+ return self.get_data()
+
+ def set_host(self, host):
+ """
+ Set the HostInfo that this node represents
+ """
+ self.set_data(host)
+
+ def get_info(self, info):
+ """Return various information extracted from the host set with
+ set_host."""
+ host = self.get_data()
+ if host is not None:
+ if info == "number_of_open_ports":
+ return host.get_port_count_by_states(["open"])
+ elif info == "vulnerability_score":
+ num_open_ports = host.get_port_count_by_states(["open"])
+ if num_open_ports < 3:
+ return 0
+ elif num_open_ports < 7:
+ return 1
+ else:
+ return 2
+ elif info == "addresses":
+ addresses = []
+ if host.ip is not None:
+ addresses.append(host.ip)
+ if host.ipv6 is not None:
+ addresses.append(host.ipv6)
+ if host.mac is not None:
+ addresses.append(host.mac)
+ return addresses
+ elif info == "ip":
+ for addr in (host.ip, host.ipv6, host.mac):
+ if addr:
+ return addr.get("addr")
+ elif info == "hostnames":
+ hostnames = []
+ for hostname in host.hostnames:
+ copy = {}
+ copy["name"] = hostname.get("hostname", "")
+ copy["type"] = hostname.get("hostname_type", "")
+ hostnames.append(copy)
+ return hostnames
+ elif info == "hostname":
+ return host.get_hostname()
+ elif info == "uptime":
+ if host.uptime.get("seconds") or host.uptime.get("lastboot"):
+ return host.uptime
+ elif info == "device_type":
+ osmatch = host.get_best_osmatch()
+ if osmatch is None:
+ return None
+ osclasses = osmatch['osclasses']
+ if len(osclasses) == 0:
+ return None
+ types = ["router", "wap", "switch", "firewall"]
+ for type in types:
+ if type in osclasses[0].get("type", "").lower():
+ return type
+ elif info == "os":
+ os = {}
+
+ # osmatches
+ if len(host.osmatches) > 0 and \
+ host.osmatches[0]["accuracy"] != "" and \
+ host.osmatches[0]["name"] != "":
+ if os is None:
+ os = {}
+ os["matches"] = host.osmatches
+ os["matches"][0]["db_line"] = 0 # not supported
+
+ os_classes = []
+ for osclass in host.osmatches[0]["osclasses"]:
+ os_class = {}
+
+ os_class["type"] = osclass.get("type", "")
+ os_class["vendor"] = osclass.get("vendor", "")
+ os_class["accuracy"] = osclass.get("accuracy", "")
+ os_class["os_family"] = osclass.get("osfamily", "")
+ os_class["os_gen"] = osclass.get("osgen", "")
+
+ os_classes.append(os_class)
+ os["classes"] = os_classes
+
+ # ports_used
+ if len(host.ports_used) > 0:
+ if os is None:
+ os = {}
+ os_portsused = []
+
+ for portused in host.ports_used:
+ os_portused = {}
+
+ os_portused["state"] = portused.get("state", "")
+ os_portused["protocol"] = portused.get("proto", "")
+ os_portused["id"] = int(portused.get("portid", "0"))
+
+ os_portsused.append(os_portused)
+
+ os["used_ports"] = os_portsused
+
+ if len(os) > 0:
+ os["fingerprint"] = ""
+ return os
+ elif info == "sequences":
+ # getting sequences information
+ sequences = {}
+ # If all fields are empty, we don't put it into the sequences
+ # list
+ if reduce(lambda x, y: x + y,
+ host.tcpsequence.values(), "") != "":
+ tcp = {}
+ if host.tcpsequence.get("index", "") != "":
+ tcp["index"] = int(host.tcpsequence["index"])
+ else:
+ tcp["index"] = 0
+ tcp["class"] = "" # not supported
+ tcp["values"] = host.tcpsequence.get(
+ "values", "").split(",")
+ tcp["difficulty"] = host.tcpsequence.get("difficulty", "")
+ sequences["tcp"] = tcp
+ if reduce(lambda x, y: x + y,
+ host.ipidsequence.values(), "") != "":
+ ip_id = {}
+ ip_id["class"] = host.ipidsequence.get("class", "")
+ ip_id["values"] = host.ipidsequence.get(
+ "values", "").split(",")
+ sequences["ip_id"] = ip_id
+ if reduce(lambda x, y: x + y,
+ host.tcptssequence.values(), "") != "":
+ tcp_ts = {}
+ tcp_ts["class"] = host.tcptssequence.get("class", "")
+ tcp_ts["values"] = host.tcptssequence.get(
+ "values", "").split(",")
+ sequences["tcp_ts"] = tcp_ts
+ return sequences
+ elif info == "filtered":
+ if (len(host.extraports) > 0 and
+ host.extraports[0]["state"] == "filtered"):
+ return True
+ else:
+ for port in host.ports:
+ if port["port_state"] == "filtered":
+ return True
+ return False
+ elif info == "ports":
+ ports = list()
+ for host_port in host.ports:
+ port = dict()
+ state = dict()
+ service = dict()
+
+ port["id"] = int(host_port.get("portid", ""))
+ port["protocol"] = host_port.get("protocol", "")
+
+ state["state"] = host_port.get("port_state", "")
+ state["reason"] = "" # not supported
+ state["reason_ttl"] = "" # not supported
+ state["reason_ip"] = "" # not supported
+
+ service["name"] = host_port.get("service_name", "")
+ service["conf"] = host_port.get("service_conf", "")
+ service["method"] = host_port.get("service_method", "")
+ service["version"] = host_port.get("service_version", "")
+ service["product"] = host_port.get("service_product", "")
+ service["extrainfo"] = host_port.get(
+ "service_extrainfo", "")
+
+ port["state"] = state
+ port["scripts"] = None # not supported
+ port["service"] = service
+
+ ports.append(port)
+ return ports
+ elif info == "extraports":
+ # extraports
+ all_extraports = list()
+ for extraport in host.extraports:
+ extraports = dict()
+ extraports["count"] = int(extraport.get("count", ""))
+ extraports["state"] = extraport.get("state", "")
+ extraports["reason"] = list() # not supported
+ extraports["all_reason"] = list() # not supported
+
+ all_extraports.append(extraports)
+ return all_extraports
+ elif info == "trace":
+ # getting traceroute information
+ if len(host.trace) > 0:
+ trace = {}
+ hops = []
+
+ for host_hop in host.trace.get("hops", []):
+ hop = {}
+ hop["ip"] = host_hop.get("ipaddr", "")
+ hop["ttl"] = int(host_hop.get("ttl", ""))
+ hop["rtt"] = host_hop.get("rtt", "")
+ hop["hostname"] = host_hop.get("host", "")
+
+ hops.append(hop)
+
+ trace["hops"] = hops
+ trace["port"] = host.trace.get("port", "")
+ trace["protocol"] = host.trace.get("proto", "")
+
+ return trace
+ else: # host is None
+ pass
+
+ return None
+
+ def get_coordinate_theta(self):
+ """
+ """
+ return self.__coordinate.get_theta()
+
+ def get_coordinate_radius(self):
+ """
+ """
+ return self.__coordinate.get_radius()
+
+ def set_coordinate_theta(self, value):
+ """
+ """
+ self.__coordinate.set_theta(value)
+
+ def set_coordinate_radius(self, value):
+ """
+ """
+ self.__coordinate.set_radius(value)
+
+ def set_polar_coordinate(self, r, t):
+ """
+ Set polar coordinate
+ @type r: number
+ @param r: The radius of coordinate
+ @type t: number
+ @param t: The angle (theta) of coordinate in radians
+ """
+ self.__coordinate.set_coordinate(r, t)
+
+ def get_polar_coordinate(self):
+ """
+ Get cartesian coordinate
+ @rtype: tuple
+ @return: Cartesian coordinates (x, y)
+ """
+ return self.__coordinate.get_coordinate()
+
+ def set_cartesian_coordinate(self, x, y):
+ """
+ Set cartesian coordinate
+ """
+ cartesian = CartesianCoordinate(x, y)
+ r, t = cartesian.to_polar()
+
+ self.set_polar_coordinate(r, math.degrees(t))
+
+ def get_cartesian_coordinate(self):
+ """
+ Get cartesian coordinate
+ @rtype: tuple
+ @return: Cartesian coordinates (x, y)
+ """
+ return self.__coordinate.to_cartesian()
+
+ def get_draw_info(self, info=None):
+ """
+ Get draw information about node
+ @type : string
+ @param : Information name
+ @rtype: mixed
+ @return: The requested information
+ """
+ if info is None:
+ return self.__draw_info
+
+ return self.__draw_info.get(info)
+
+ def set_draw_info(self, info):
+ """
+ Set draw information
+ @type : dict
+ @param : Draw information dictionary
+ """
+ for key in info:
+ self.__draw_info[key] = info[key]
+
+ def deep_search_child(self, node):
+ """
+ """
+ for child in self.get_draw_info('children'):
+
+ if child == node:
+ return True
+
+ elif child.deep_search_child(node):
+ return True
+
+ return False
+
+ def set_subtree_info(self, info):
+ """
+ """
+ for child in self.get_draw_info('children'):
+
+ child.set_draw_info(info)
+
+ if not child.get_draw_info('group'):
+ child.set_subtree_info(info)
+
+ def calc_needed_space(self):
+ """
+ """
+ number_of_children = len(self.get_draw_info('children'))
+
+ sum_angle = 0
+ own_angle = 0
+
+ if number_of_children > 0 and not self.get_draw_info('group'):
+
+ for child in self.get_draw_info('children'):
+
+ child.calc_needed_space()
+ sum_angle += child.get_draw_info('space_need')
+
+ distance = self.get_coordinate_radius()
+ size = self.get_draw_info('radius') * 2
+ own_angle = geometry.angle_from_object(distance, size)
+
+ self.set_draw_info({'children_need': sum_angle})
+ self.set_draw_info({'space_need': max(sum_angle, own_angle)})
diff --git a/zenmap/radialnet/gui/SaveDialog.py b/zenmap/radialnet/gui/SaveDialog.py
new file mode 100644
index 0000000..60cfbbe
--- /dev/null
+++ b/zenmap/radialnet/gui/SaveDialog.py
@@ -0,0 +1,169 @@
+# vim: set encoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+import os.path
+import radialnet.gui.RadialNet as RadialNet
+import zenmapGUI.FileChoosers
+
+from zenmapGUI.higwidgets.higboxes import HIGHBox
+from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog
+
+
+TYPES = ((_("By extension"), None, None),
+ ("PDF", RadialNet.FILE_TYPE_PDF, ".pdf"),
+ ("PNG", RadialNet.FILE_TYPE_PNG, ".png"),
+ ("PostScript", RadialNet.FILE_TYPE_PS, ".ps"),
+ ("SVG", RadialNet.FILE_TYPE_SVG, ".svg"))
+# Build a reverse index of extensions to file types, for the "By extension"
+# file type.
+EXTENSIONS = {}
+for type in TYPES:
+ if type[2] is not None:
+ EXTENSIONS[type[2]] = type[1]
+
+
+class SaveDialog(Gtk.FileChooserDialog):
+ def __init__(self):
+ """
+ """
+ super(SaveDialog, self).__init__(title=_("Save Topology"),
+ action=Gtk.FileChooserAction.SAVE,
+ buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+
+ types_store = Gtk.ListStore.new([str, object, str])
+ for type in TYPES:
+ types_store.append(type)
+
+ self.__combo = Gtk.ComboBox.new_with_model(types_store)
+ cell = Gtk.CellRendererText()
+ self.__combo.pack_start(cell, True)
+ self.__combo.add_attribute(cell, "text", 0)
+
+ self.__combo.connect("changed", self.__combo_changed_cb)
+ self.__combo.set_active(0)
+
+ self.connect("response", self.__response_cb)
+
+ hbox = HIGHBox()
+ label = Gtk.Label.new(_("Select File Type:"))
+ hbox.pack_end(self.__combo, False, True, 0)
+ hbox.pack_end(label, False, True, 0)
+
+ self.set_extra_widget(hbox)
+ self.set_do_overwrite_confirmation(True)
+
+ hbox.show_all()
+
+ def __combo_changed_cb(self, widget):
+ filename = self.get_filename() or ""
+ dir, basename = os.path.split(filename)
+ if dir != self.get_current_folder():
+ self.set_current_folder(dir)
+
+ # Find the recommended extension.
+ new_ext = self.__combo.get_model().get_value(
+ self.__combo.get_active_iter(), 2)
+ if new_ext is not None:
+ # Change the filename to use the recommended extension.
+ root, ext = os.path.splitext(basename)
+ if len(ext) == 0 and root.startswith("."):
+ root = ""
+ self.set_current_name(root + new_ext)
+
+ def __response_cb(self, widget, response_id):
+ """Intercept the "response" signal to check if someone used the "By
+ extension" file type with an unknown extension."""
+ if response_id == Gtk.ResponseType.OK and self.get_filetype() is None:
+ ext = self.__get_extension()
+ if ext == "":
+ filename = self.get_filename() or ""
+ dir, basename = os.path.split(filename)
+ alert = HIGAlertDialog(
+ message_format=_("No filename extension"),
+ secondary_text=_("""\
+The filename "%s" does not have an extension, \
+and no specific file type was chosen.
+Enter a known extension or select the file type from the list.""" % basename))
+
+ else:
+ alert = HIGAlertDialog(
+ message_format=_("Unknown filename extension"),
+ secondary_text=_("""\
+There is no file type known for the filename extension "%s".
+Enter a known extension or select the file type from the list.\
+""") % self.__get_extension())
+ alert.run()
+ alert.destroy()
+ # Go back to the dialog.
+ self.emit_stop_by_name("response")
+
+ def __get_extension(self):
+ return os.path.splitext(self.get_filename())[1]
+
+ def get_filetype(self):
+ filetype = self.__combo.get_model().get_value(
+ self.__combo.get_active_iter(), 1)
+ if filetype is None:
+ # Guess based on extension.
+ return EXTENSIONS.get(self.__get_extension())
+ return filetype
diff --git a/zenmap/radialnet/gui/Toolbar.py b/zenmap/radialnet/gui/Toolbar.py
new file mode 100644
index 0000000..23b7077
--- /dev/null
+++ b/zenmap/radialnet/gui/Toolbar.py
@@ -0,0 +1,309 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+from radialnet.bestwidgets.buttons import BWStockButton, BWToggleStockButton
+from radialnet.gui.SaveDialog import SaveDialog
+from radialnet.gui.Dialogs import AboutDialog
+from radialnet.gui.LegendWindow import LegendWindow
+from radialnet.gui.HostsViewer import HostsViewer
+from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog
+
+
+SHOW = True
+HIDE = False
+
+REFRESH_RATE = 500
+
+
+class ToolsMenu(Gtk.Menu):
+ """
+ """
+ def __init__(self, radialnet):
+ """
+ """
+ Gtk.Menu.__init__(self)
+
+ self.radialnet = radialnet
+
+ self.__create_items()
+
+ def __create_items(self):
+ """
+ """
+ self.__hosts = Gtk.ImageMenuItem.new_with_label(_('Hosts viewer'))
+ self.__hosts.connect("activate", self.__hosts_viewer_callback)
+ self.__hosts_image = Gtk.Image()
+ self.__hosts_image.set_from_stock(Gtk.STOCK_INDEX, Gtk.IconSize.MENU)
+ self.__hosts.set_image(self.__hosts_image)
+
+ self.append(self.__hosts)
+
+ self.__hosts.show_all()
+
+ def __hosts_viewer_callback(self, widget):
+ """
+ """
+ window = HostsViewer(self.radialnet.get_scanned_nodes())
+ window.show_all()
+ window.set_keep_above(True)
+
+ def enable_dependents(self):
+ """
+ """
+ self.__hosts.set_sensitive(True)
+
+ def disable_dependents(self):
+ """
+ """
+ self.__hosts.set_sensitive(False)
+
+
+class Toolbar(Gtk.Box):
+ """
+ """
+ def __init__(self, radialnet, window, control, fisheye):
+ """
+ """
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
+ #self.set_style(gtk.TOOLBAR_BOTH_HORIZ)
+ #self.set_tooltips(True)
+
+ self.radialnet = radialnet
+
+ self.__window = window
+ self.__control_widget = control
+ self.__fisheye_widget = fisheye
+
+ self.__control_widget.show_all()
+ self.__control_widget.set_no_show_all(True)
+ self.__control_widget.hide()
+
+ self.__fisheye_widget.show_all()
+ self.__fisheye_widget.set_no_show_all(True)
+ self.__fisheye_widget.hide()
+
+ self.__save_chooser = None
+
+ self.__create_widgets()
+
+ def __create_widgets(self):
+ """
+ """
+ # self.__tooltips = gtk.Tooltips()
+
+ #self.__tools_menu = ToolsMenu(self.radialnet)
+
+ #self.__tools_button = gtk.MenuToolButton(gtk.STOCK_PREFERENCES)
+ #self.__tools_button.set_label(_('Tools'))
+ #self.__tools_button.set_is_important(True)
+ #self.__tools_button.set_menu(self.__tools_menu)
+ #self.__tools_button.connect('clicked', self.__tools_callback)
+
+ self.__save_button = BWStockButton(Gtk.STOCK_SAVE, _("Save Graphic"))
+ self.__save_button.connect("clicked", self.__save_image_callback)
+
+ self.__hosts_button = BWStockButton(Gtk.STOCK_INDEX, _("Hosts Viewer"))
+ self.__hosts_button.connect("clicked", self.__hosts_viewer_callback)
+
+ self.__control = BWToggleStockButton(
+ Gtk.STOCK_PROPERTIES, _("Controls"))
+ self.__control.connect('clicked', self.__control_callback)
+ self.__control.set_active(False)
+
+ self.__fisheye = BWToggleStockButton(Gtk.STOCK_ZOOM_FIT, _("Fisheye"))
+ self.__fisheye.connect('clicked', self.__fisheye_callback)
+ self.__fisheye.set_active(False)
+
+ self.__legend_button = BWStockButton(Gtk.STOCK_INDEX, _("Legend"))
+ self.__legend_button.connect('clicked', self.__legend_callback)
+
+ #self.__fullscreen = gtk.ToggleToolButton(gtk.STOCK_FULLSCREEN)
+ #self.__fullscreen.set_label(_('Fullscreen'))
+ #self.__fullscreen.set_is_important(True)
+ #self.__fullscreen.connect('clicked', self.__fullscreen_callback)
+ #self.__fullscreen.set_tooltip(self.__tooltips, _('Toggle fullscreen'))
+
+ #self.__about = gtk.ToolButton(gtk.STOCK_ABOUT)
+ #self.__about.set_label(_('About'))
+ #self.__about.set_is_important(True)
+ #self.__about.connect('clicked', self.__about_callback)
+ #self.__about.set_tooltip(self.__tooltips, _('About RadialNet'))
+
+ self.__separator = Gtk.SeparatorToolItem()
+ self.__expander = Gtk.SeparatorToolItem()
+ self.__expander.set_expand(True)
+ self.__expander.set_draw(False)
+
+ #self.insert(self.__open, 0)
+ #self.insert(self.__separator, 1)
+ #self.insert(self.__tools_button, 2)
+ #self.insert(self.__expander, 3)
+ #self.insert(self.__control, 4)
+ #self.insert(self.__fisheye, 5)
+ #self.insert(self.__fullscreen, 6)
+ #self.insert(self.__about, 7)
+
+ #self.pack_start(self.__tools_button, False)
+ self.pack_start(self.__hosts_button, False, True, 0)
+ self.pack_start(self.__fisheye, False, True, 0)
+ self.pack_start(self.__control, False, True, 0)
+ self.pack_end(self.__save_button, False, True, 0)
+ self.pack_end(self.__legend_button, False, True, 0)
+
+ def disable_controls(self):
+ """
+ """
+ self.__control.set_sensitive(False)
+ self.__fisheye.set_sensitive(False)
+ self.__hosts_button.set_sensitive(False)
+ self.__legend_button.set_sensitive(False)
+ #self.__tools_menu.disable_dependents()
+
+ def enable_controls(self):
+ """
+ """
+ self.__control.set_sensitive(True)
+ self.__fisheye.set_sensitive(True)
+ self.__hosts_button.set_sensitive(True)
+ self.__legend_button.set_sensitive(True)
+ #self.__tools_menu.enable_dependents()
+
+ def __tools_callback(self, widget):
+ """
+ """
+ self.__tools_menu.popup(None, None, None, 1, 0)
+
+ def __hosts_viewer_callback(self, widget):
+ """
+ """
+ window = HostsViewer(self.radialnet.get_scanned_nodes())
+ window.show_all()
+ window.set_keep_above(True)
+
+ def __save_image_callback(self, widget):
+ """
+ """
+ if self.__save_chooser is None:
+ self.__save_chooser = SaveDialog()
+
+ response = self.__save_chooser.run()
+
+ if response == Gtk.ResponseType.OK:
+ filename = self.__save_chooser.get_filename()
+ filetype = self.__save_chooser.get_filetype()
+
+ try:
+ self.radialnet.save_drawing_to_file(filename, filetype)
+ except Exception as e:
+ alert = HIGAlertDialog(parent=self.__save_chooser,
+ type=Gtk.MessageType.ERROR,
+ message_format=_("Error saving snapshot"),
+ secondary_text=str(e))
+ alert.run()
+ alert.destroy()
+
+ self.__save_chooser.hide()
+
+ def __control_callback(self, widget=None):
+ """
+ """
+ if self.__control.get_active():
+ self.__control_widget.show()
+
+ else:
+ self.__control_widget.hide()
+
+ def __fisheye_callback(self, widget=None):
+ """
+ """
+ if not self.radialnet.is_in_animation():
+
+ if self.__fisheye.get_active():
+
+ self.__fisheye_widget.active_fisheye()
+ self.__fisheye_widget.show()
+
+ else:
+
+ self.__fisheye_widget.deactive_fisheye()
+ self.__fisheye_widget.hide()
+
+ def __legend_callback(self, widget):
+ """
+ """
+ self.__legend_window = LegendWindow()
+ self.__legend_window.show_all()
+
+ def __about_callback(self, widget):
+ """
+ """
+ self.__about_dialog = AboutDialog()
+ self.__about_dialog.show_all()
+
+ def __fullscreen_callback(self, widget=None):
+ """
+ """
+ if self.__fullscreen.get_active():
+ self.__window.fullscreen()
+
+ else:
+ self.__window.unfullscreen()
diff --git a/zenmap/radialnet/gui/__init__.py b/zenmap/radialnet/gui/__init__.py
new file mode 100644
index 0000000..c314dd7
--- /dev/null
+++ b/zenmap/radialnet/gui/__init__.py
@@ -0,0 +1,56 @@
+# vim: set fileencoding=utf-8 :
+
+# ***********************IMPORTANT NMAP LICENSE TERMS************************
+# *
+# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap
+# * Project"). Nmap is also a registered trademark of the Nmap Project.
+# *
+# * This program is distributed under the terms of the Nmap Public Source
+# * License (NPSL). The exact license text applying to a particular Nmap
+# * release or source code control revision is contained in the LICENSE
+# * file distributed with that version of Nmap or source code control
+# * revision. More Nmap copyright/legal information is available from
+# * https://nmap.org/book/man-legal.html, and further information on the
+# * NPSL license itself can be found at https://nmap.org/npsl/ . This
+# * header summarizes some key points from the Nmap license, but is no
+# * substitute for the actual license text.
+# *
+# * Nmap is generally free for end users to download and use themselves,
+# * including commercial use. It is available from https://nmap.org.
+# *
+# * The Nmap license generally prohibits companies from using and
+# * redistributing Nmap in commercial products, but we sell a special Nmap
+# * OEM Edition with a more permissive license and special features for
+# * this purpose. See https://nmap.org/oem/
+# *
+# * If you have received a written Nmap license agreement or contract
+# * stating terms other than these (such as an Nmap OEM license), you may
+# * choose to use and redistribute Nmap under those terms instead.
+# *
+# * The official Nmap Windows builds include the Npcap software
+# * (https://npcap.com) for packet capture and transmission. It is under
+# * separate license terms which forbid redistribution without special
+# * permission. So the official Nmap Windows builds may not be redistributed
+# * without special permission (such as an Nmap OEM license).
+# *
+# * Source is provided to this software because we believe users have a
+# * right to know exactly what a program is going to do before they run it.
+# * This also allows you to audit the software for security holes.
+# *
+# * Source code also allows you to port Nmap to new platforms, fix bugs, and add
+# * new features. You are highly encouraged to submit your changes as a Github PR
+# * or by email to the dev@nmap.org mailing list for possible incorporation into
+# * the main distribution. Unless you specify otherwise, it is understood that
+# * you are offering us very broad rights to use your submissions as described in
+# * the Nmap Public Source License Contributor Agreement. This is important
+# * because we fund the project by selling licenses with various terms, and also
+# * because the inability to relicense code has caused devastating problems for
+# * other Free Software projects (such as KDE and NASM).
+# *
+# * The free version of Nmap 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. Warranties,
+# * indemnification and commercial support are all available through the
+# * Npcap OEM program--see https://nmap.org/oem/
+# *
+# ***************************************************************************/