From 5bd15ad0d14e925d1f42ff12a1bc254de3fbdda8 Mon Sep 17 00:00:00 2001
From: Cyrille Bagard <nocbos@gmail.com>
Date: Thu, 24 Dec 2020 17:34:38 +0100
Subject: Created the basics of a new panel for binary content glimpse.

---
 configure.ac                        |   1 +
 plugins/python/Makefile.am          |   1 +
 plugins/python/cglimpse/Makefile.am |  10 ++
 plugins/python/cglimpse/__init__.py |   2 +
 plugins/python/cglimpse/core.py     |  24 +++
 plugins/python/cglimpse/distro.py   | 101 +++++++++++
 plugins/python/cglimpse/entropy.py  | 158 ++++++++++++++++++
 plugins/python/cglimpse/method.py   |  43 +++++
 plugins/python/cglimpse/panel.py    | 323 ++++++++++++++++++++++++++++++++++++
 plugins/python/cglimpse/panel.ui    | 299 +++++++++++++++++++++++++++++++++
 10 files changed, 962 insertions(+)
 create mode 100644 plugins/python/cglimpse/Makefile.am
 create mode 100644 plugins/python/cglimpse/__init__.py
 create mode 100644 plugins/python/cglimpse/core.py
 create mode 100644 plugins/python/cglimpse/distro.py
 create mode 100644 plugins/python/cglimpse/entropy.py
 create mode 100644 plugins/python/cglimpse/method.py
 create mode 100644 plugins/python/cglimpse/panel.py
 create mode 100644 plugins/python/cglimpse/panel.ui

diff --git a/configure.ac b/configure.ac
index e57fbb6..1384faa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -493,6 +493,7 @@ AC_CONFIG_FILES([Makefile
                  plugins/python/Makefile
                  plugins/python/abackup/Makefile
                  plugins/python/apkfiles/Makefile
+                 plugins/python/cglimpse/Makefile
                  plugins/python/checksec/Makefile
                  plugins/python/liveconv/Makefile
                  plugins/python/scripting/Makefile
diff --git a/plugins/python/Makefile.am b/plugins/python/Makefile.am
index c96b6c5..67e60e9 100644
--- a/plugins/python/Makefile.am
+++ b/plugins/python/Makefile.am
@@ -2,6 +2,7 @@
 SUBDIRS = \
 	abackup \
 	apkfiles \
+	cglimpse \
 	checksec \
 	liveconv \
 	scripting
diff --git a/plugins/python/cglimpse/Makefile.am b/plugins/python/cglimpse/Makefile.am
new file mode 100644
index 0000000..8534bf8
--- /dev/null
+++ b/plugins/python/cglimpse/Makefile.am
@@ -0,0 +1,10 @@
+
+cglimpsedir = $(pluginsdatadir)/python/cglimpse
+
+cglimpse_DATA = 							\
+	__init__.py								\
+	core.py									\
+	panel.py								\
+	panel.ui
+
+EXTRA_DIST = $(cglimpse_DATA)
diff --git a/plugins/python/cglimpse/__init__.py b/plugins/python/cglimpse/__init__.py
new file mode 100644
index 0000000..7085317
--- /dev/null
+++ b/plugins/python/cglimpse/__init__.py
@@ -0,0 +1,2 @@
+
+from cglimpse.core import ContentGlimpse as AutoLoad
diff --git a/plugins/python/cglimpse/core.py b/plugins/python/cglimpse/core.py
new file mode 100644
index 0000000..d2cfdc4
--- /dev/null
+++ b/plugins/python/cglimpse/core.py
@@ -0,0 +1,24 @@
+
+from pychrysalide import PluginModule
+from pychrysalide.gui import core
+
+from .panel import CGlimpsePanel
+
+
+class ContentGlimpse(PluginModule):
+    """Convert raw values into interpreted values."""
+
+    _name = 'ContentGlimpse'
+    _desc = 'Display some glimpses of binary contents'
+    _version = '0.1'
+    _url = 'https://www.chrysalide.re/'
+
+    _actions = ( )
+
+
+    def __init__(self):
+        """Initialize the plugin for Chrysalide."""
+
+        super(ContentGlimpse, self).__init__()
+
+        core.register_panel(CGlimpsePanel)
diff --git a/plugins/python/cglimpse/distro.py b/plugins/python/cglimpse/distro.py
new file mode 100644
index 0000000..5ffc523
--- /dev/null
+++ b/plugins/python/cglimpse/distro.py
@@ -0,0 +1,101 @@
+
+from .method import GlimpseMethod
+
+
+class ByteDistribution(GlimpseMethod):
+
+    def __init__(self, builder):
+        """Prepare a Shannon entropy display."""
+
+        super(ByteDistribution, self).__init__(builder)
+
+        button = builder.get_object('shannon_color')
+        button.connect('color-set', self._on_color_set)
+
+        self._on_color_set(button)
+
+        self._step = 0x80
+
+        self._v_legend = 'Quantity'
+        self._h_legend = 'Byte values'
+
+        self._x_range = [ 0, 0x20, 0x100 ]
+        self._y_range = [ 0, 25, 100 ]
+
+        self._values = {}
+
+
+    def _on_color_set(self, button):
+        """React on color chosen for the rendering."""
+
+        color = button.get_rgba()
+        self._color = [ color.red, color.green, color.blue, color.alpha ]
+        self._shadow_color = [ color.red * 0.5, color.green * 0.5, color.blue * 0.5, color.alpha ]
+
+
+    def format_legend(self, value, vert):
+        """Build the label used for a rule."""
+
+        if vert:
+            text = str(value)
+
+        else:
+
+            scale = [ ' kb', ' Mb', ' Gb', ' Tb' ]
+            suffix = ''
+
+            for i in range(len(scale)):
+
+                if value < 1024:
+                    break
+
+                value /= 1024
+                suffix = scale[i]
+
+            text = '%u%s' % (value, suffix)
+
+        return text
+
+
+    def update(self, data):
+        """Provide a description for the method."""
+
+        max_count = 0
+
+        self._values = {}
+
+        for i in range(256):
+            self._values[i] = 0
+
+        for b in data:
+
+            if b in self._values.keys():
+                self._values[b] += 1
+
+        for c in self._values.values():
+            if c > max_count:
+                max_count = c
+
+        self._y_range = [ 0, max_count / 4, max_count ]
+
+
+    def render(self, cr, area):
+        """Draw the bytes distribution for the current binary, if any."""
+
+        max_count = self._y_range[-1]
+
+        last_x = area[0]
+
+        cr.set_source_rgba(*self._shadow_color)
+        cr.set_source_rgba(*self._color)
+        cr.set_line_width(1)
+
+        for i in range(256):
+
+            x = area[0] + ((i + 1) * area[2]) / 256
+            h = (area[3] * self._values[i]) / max_count
+
+            cr.rectangle(last_x, area[1] + area[3] - h, x - last_x, h)
+            cr.fill()
+
+            last_x = x
diff --git a/plugins/python/cglimpse/entropy.py b/plugins/python/cglimpse/entropy.py
new file mode 100644
index 0000000..f090624
--- /dev/null
+++ b/plugins/python/cglimpse/entropy.py
@@ -0,0 +1,158 @@
+
+import math
+
+from .method import GlimpseMethod
+
+
+class ShannonEntropy(GlimpseMethod):
+
+    def __init__(self, builder):
+        """Prepare a Shannon entropy display."""
+
+        super(ShannonEntropy, self).__init__(builder)
+
+        button = builder.get_object('shannon_color')
+        button.connect('color-set', self._on_color_set)
+
+        self._on_color_set(button)
+
+        self._step = 0x80
+
+        self._v_legend = 'Entropy'
+        self._h_legend = 'Byte offsets'
+
+        self._x_range = [ 0, 1024, 10240 ]
+        self._y_range = [ 0.0, 0.25, 1.0 ]
+
+        self._size = None
+        self._values = []
+
+
+    def _on_color_set(self, button):
+        """React on color chosen for the rendering."""
+
+        color = button.get_rgba()
+        self._color = [ color.red, color.green, color.blue, color.alpha ]
+        self._shadow_color = [ color.red * 0.5, color.green * 0.5, color.blue * 0.5, color.alpha ]
+
+
+    def format_legend(self, value, vert):
+        """Build the label used for a rule."""
+
+        if vert:
+            text = str(value)
+
+        else:
+
+            scale = [ ' kb', ' Mb', ' Gb', ' Tb' ]
+            suffix = ''
+
+            for i in range(len(scale)):
+
+                if value < 1024:
+                    break
+
+                value /= 1024
+                suffix = scale[i]
+
+            text = '%u%s' % (value, suffix)
+
+        return text
+
+
+    def update(self, data):
+        """Provide a description for the method."""
+
+        self._size = len(data)
+
+        step = 2 ** math.ceil(math.log(self._size / 10, 2))
+
+        self._x_range = [ 0, step, 10 * step ]
+
+        self._values = []
+
+        for i in range(0, self._size, self._step):
+
+            counter = [ 0 for i in range(256) ]
+
+            start = i
+            end = i + self._step
+
+            if end > self._size:
+                end = self._size
+
+            for b in data[start : end]:
+                counter[b] += 1
+
+            ent = 0.0
+
+            for c in counter:
+                if c > 0:
+                    freq = c / (end - start)
+                    ent += freq * math.log(freq, 256)
+
+            self._values.append(-ent)
+
+
+    def render(self, cr, area):
+        """Draw the bytes distribution for the current binary, if any."""
+
+        step = 2 ** math.ceil(math.log(self._size / 10, 2))
+
+        if self._size % step == 0:
+            full_size = self._size
+        else:
+            full_size = (self._size + step - 1) & ~(step - 1)
+
+        start = 0
+        last_x = area[0]
+
+        last_y = area[1] + area[3] - (area[3] * self._values[0])
+        cr.move_to(last_x, last_y + 2)
+
+        for i in range(0, self._size, self._step):
+
+            end = i + self._step
+
+            if end > self._size:
+                end = self._size
+
+            x = area[0] + ((end - start) * area[2]) / full_size
+            y = area[1] + area[3] - (area[3] * self._values[int(i / self._step)])
+
+            if last_y != y:
+                cr.line_to(last_x, y + 2)
+
+            cr.line_to(x, y + 2)
+
+            last_x = x
+
+        cr.set_source_rgba(*self._shadow_color)
+        cr.set_line_width(4)
+        cr.stroke()
+
+        last_x = area[0]
+
+        last_y = area[1] + area[3] - (area[3] * self._values[0])
+        cr.move_to(last_x, last_y)
+
+        for i in range(0, self._size, self._step):
+
+            end = i + self._step
+
+            if end > self._size:
+                end = self._size
+
+            x = area[0] + ((end - start) * area[2]) / full_size
+            y = area[1] + area[3] - (area[3] * self._values[int(i / self._step)])
+
+            if last_y != y:
+                cr.line_to(last_x, y)
+
+            cr.line_to(x, y)
+
+            last_x = x
+
+        cr.set_source_rgba(*self._color)
+        cr.set_line_width(2)
+        cr.stroke()
diff --git a/plugins/python/cglimpse/method.py b/plugins/python/cglimpse/method.py
new file mode 100644
index 0000000..f2e9fdb
--- /dev/null
+++ b/plugins/python/cglimpse/method.py
@@ -0,0 +1,43 @@
+
+class GlimpseMethod():
+    """Abstract class for gimpses."""
+
+
+    def __init__(self, builder):
+        """Populate the class with its attributes."""
+
+        self._builder = builder
+
+        self._v_legend = None
+        self._h_legend = None
+
+        self._x_range = None
+        self._y_range = None
+
+
+    def switch(self):
+        """Switch the panel labels to the method ones."""
+
+        lbl = self._builder.get_object('v_legend')
+        lbl.set_text(self._v_legend)
+
+        lbl = self._builder.get_object('h_legend')
+        lbl.set_text(self._h_legend)
+
+
+    def setup_rendering(self):
+        """Provide information useful for drawing the grid."""
+
+        return self._x_range, self._y_range
+
+
+    def format_legend(self, value, vert):
+        """Build the label used for a rule."""
+
+        return str(value)
+
+
+    def render(self, cr, area):
+        """Draw the bytes distribution for the current binary, if any."""
+
+        pass
diff --git a/plugins/python/cglimpse/panel.py b/plugins/python/cglimpse/panel.py
new file mode 100644
index 0000000..2fd3c7f
--- /dev/null
+++ b/plugins/python/cglimpse/panel.py
@@ -0,0 +1,323 @@
+
+import os
+from gi.repository import Gtk
+from pychrysalide import core
+from pychrysalide.gtkext import BuiltNamedWidget
+from pychrysalide.gtkext import EasyGtk
+from pychrysalide.gui import PanelItem
+from pychrysalide.gui.panels import UpdatablePanel
+
+
+from .distro import ByteDistribution
+from .entropy import ShannonEntropy
+
+
+class CGlimpsePanel(PanelItem, UpdatablePanel):
+
+    _key = 'cglimpse'
+
+    _path = 'Ms'
+
+    _working_group_id = core.setup_tiny_global_work_group()
+
+
+    def __init__(self):
+        """Initialize the GUI panel."""
+
+        directory = os.path.dirname(os.path.realpath(__file__))
+        filename = os.path.join(directory, 'panel.ui')
+
+        widget = BuiltNamedWidget('Content glimpse', 'Binary content glimpse', filename)
+
+        super(CGlimpsePanel, self).__init__(widget)
+
+        self._builder = self.named_widget.builder
+
+        self._builder.connect_signals(self)
+
+        self._content = None
+
+        combo = self._builder.get_object('method_sel')
+        self._on_method_changed(combo)
+
+
+    def _change_content(self, old, new):
+        """Get notified about a LoadedContent change."""
+
+        self._content = new.content
+
+        combo = self._builder.get_object('method_sel')
+        self._on_method_changed(combo)
+
+
+    def _setup(self, uid):
+        """Prepare an update process for a panel."""
+
+        assert(uid == 0)
+
+        return ( 1, {}, 'Computing data for content glimpse' )
+
+
+    def _introduce(self, uid, data):
+        """Introduce the update process and switch display."""
+
+        assert(uid == 0)
+
+        self.switch_to_updating_mask()
+
+
+    def _process(self, uid, status, id, data):
+        """Perform the computing of data to render."""
+
+        assert(uid == 0)
+
+        self._current.update(self._content.data)
+
+        area = self._builder.get_object('content')
+        area.queue_draw()
+
+
+    def _conclude(self, uid, data):
+        """Conclude the update process and display the computed data."""
+
+        assert(uid == 0)
+
+        self.switch_to_updated_content()
+
+
+    def _clean_data(self, uid, data):
+        """Delete dynamically generated objects for the panel update."""
+
+        # Not really useful here...
+
+        assert(uid == 0)
+
+
+    def _on_method_changed(self, combo):
+        """React on method selection change."""
+
+        tree_iter = combo.get_active_iter()
+
+        if tree_iter:
+
+            model = combo.get_model()
+            key = model[tree_iter][1]
+
+            if key == 'shanon':
+                self._current = ShannonEntropy(self._builder)
+
+            else:
+                assert(key == 'distrib')
+                self._current = ByteDistribution(self._builder)
+
+            self._current.switch()
+
+            if self._content:
+
+                self.run_update(0)
+
+
+    def _on_options_toggled(self, button):
+        """React on options display/hide order."""
+
+        lbl = self._builder.get_object('options_label')
+
+        common = self._builder.get_object('common_options')
+
+        if button.get_active():
+
+            button.get_parent().child_set_property(button, 'expand', False)
+            lbl.set_angle(0)
+
+            common.show()
+
+        else:
+
+            button.get_parent().child_set_property(button, 'expand', True)
+            lbl.set_angle(90)
+
+            common.hide()
+
+
+    def _render_grid(self, widget, cr):
+        """Draw a basic empty grid."""
+
+        # Colors
+
+        color = EasyGtk.get_color_from_style('view', True)
+        bg_color = [ color.red, color.green, color.blue, color.alpha ]
+
+        color = EasyGtk.get_color_from_style('background', False)
+        line_color = [ color.red, color.green, color.blue, color.alpha ]
+
+        # Background
+
+        w = widget.get_allocation().width
+        h = widget.get_allocation().height
+
+        cr.set_source_rgba(*bg_color)
+
+        cr.rectangle(0, 0, w, h)
+        cr.fill()
+
+        # Area definitions
+
+        x_range, y_range = self._current.setup_rendering()
+
+        margin_left = 0
+        margin_bottom = 0
+
+        y_count = int((y_range[2] - y_range[0]) / y_range[1])
+
+        for i in range(y_count + 1):
+
+            text = self._current.format_legend(y_range[0] + i * y_range[1], True)
+
+            (_, _, width, height, _, _) = cr.text_extents(text)
+
+            if width > margin_left:
+
+                margin_left = width
+                margin_bottom = height
+
+        bar_padding = 5
+        bar_tick = 3
+        arrow_size = 4
+
+        graph_left = bar_padding + margin_left + bar_tick * 3
+        graph_right = w - 2 * bar_padding
+
+        graph_bottom = h - bar_padding - margin_bottom - bar_tick * 3
+
+        data_left = graph_left + 2 * bar_padding
+        data_right = graph_right - 2 * bar_padding
+
+        data_top = 5 * bar_padding
+        data_bottom = graph_bottom - 2 * bar_padding
+
+        data_height = data_bottom - data_top
+        data_width = data_right - data_left
+
+        data_area = [ data_left, data_top, data_width, data_height ]
+
+        # Grid content #1
+
+        cr.set_source_rgba(*line_color)
+
+        cr.set_line_width(1)
+
+        cr.move_to(graph_left, 2 * bar_padding)
+        cr.line_to(graph_left, graph_bottom)
+        cr.line_to(graph_right, graph_bottom)
+
+        cr.stroke()
+
+        cr.move_to(graph_right, graph_bottom)
+        cr.line_to(graph_right - arrow_size, graph_bottom - arrow_size)
+        cr.line_to(graph_right - arrow_size, graph_bottom + arrow_size)
+
+        cr.fill()
+
+        cr.move_to(graph_left, 2 * bar_padding)
+        cr.line_to(graph_left - arrow_size, 2 * bar_padding + arrow_size)
+        cr.line_to(graph_left + arrow_size, 2 * bar_padding + arrow_size)
+
+        cr.fill()
+
+        cr.set_source_rgba(0, 0, 0, 0.2)
+
+        cr.rectangle(*data_area)
+        cr.fill()
+
+        # Grid content #2
+
+        y_count = int((y_range[2] - y_range[0]) / y_range[1])
+
+        for i in range(y_count + 1):
+
+            y = data_bottom - (i * data_height) / y_count
+
+            # Line
+
+            cr.save()
+
+            cr.set_source_rgba(*line_color[:3], line_color[3] * 0.4)
+            cr.set_dash([ 2 * bar_tick, 6 * bar_tick ])
+
+            cr.move_to(graph_left + 6 * bar_tick, y)
+            cr.line_to(data_right, y)
+
+            cr.stroke()
+
+            cr.restore()
+
+            # Tick
+
+            cr.set_source_rgba(*line_color)
+
+            cr.move_to(graph_left - bar_tick, y)
+            cr.line_to(graph_left + bar_tick, y)
+
+            cr.stroke()
+
+            # Text
+
+            text = self._current.format_legend(y_range[0] + i * y_range[1], True)
+
+            _, _, tw, th, _, _ = cr.text_extents(text)
+
+            x = graph_left - 3 * bar_tick - tw
+
+            cr.move_to(x, y + th / 2)
+            cr.show_text(text)
+
+        x_count = int((x_range[2] - x_range[0]) / x_range[1])
+
+        for i in range(x_count + 1):
+
+            x = data_left + (i * data_width) / x_count
+
+            # Line
+
+            cr.save()
+
+            cr.set_source_rgba(*line_color[:3], line_color[3] * 0.4)
+            cr.set_dash([ 2 * bar_tick, 6 * bar_tick ])
+
+            cr.move_to(x, data_top)
+            cr.line_to(x, data_bottom)
+
+            cr.stroke()
+
+            cr.restore()
+
+            # Tick
+
+            cr.set_source_rgba(*line_color)
+
+            cr.move_to(x, graph_bottom - bar_tick)
+            cr.line_to(x, graph_bottom + bar_tick)
+
+            cr.stroke()
+
+            # Text
+
+            text = self._current.format_legend(x_range[0] + i * x_range[1], False)
+
+            _, _, tw, th, _, _ = cr.text_extents(text)
+
+            y = graph_bottom + 3 * bar_tick + th
+
+            cr.move_to(x - tw / 2, y)
+            cr.show_text(text)
+
+        return data_area
+
+
+    def _render_content_glimpse(self, widget, cr):
+        """Draw the selected content view."""
+
+        data_area = self._render_grid(widget, cr)
+
+        if self._content:
+            self._current.render(cr, data_area)
diff --git a/plugins/python/cglimpse/panel.ui b/plugins/python/cglimpse/panel.ui
new file mode 100644
index 0000000..0ec6dc5
--- /dev/null
+++ b/plugins/python/cglimpse/panel.ui
@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.37.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <object class="GtkBox" id="box">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkToggleButton">
+            <property name="visible">True</property>
+            <property name="can-focus">True</property>
+            <property name="receives-default">True</property>
+            <signal name="toggled" handler="_on_options_toggled" swapped="no"/>
+            <child>
+              <object class="GtkLabel" id="options_label">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="label" translatable="yes">Options</property>
+                <property name="angle">90</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="common_options">
+            <property name="can-focus">False</property>
+            <property name="margin-start">8</property>
+            <property name="margin-end">8</property>
+            <property name="margin-top">8</property>
+            <property name="margin-bottom">8</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">8</property>
+            <child>
+              <!-- n-columns=2 n-rows=3 -->
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="row-spacing">8</property>
+                <property name="column-spacing">8</property>
+                <child>
+                  <object class="GtkComboBoxText" id="method_sel">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="active">0</property>
+                    <items>
+                      <item id="shanon" translatable="yes">Shanon entropy</item>
+                      <item id="distrib" translatable="yes">Bytes distribution</item>
+                    </items>
+                    <signal name="changed" handler="_on_method_changed" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">0</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label" translatable="yes">Start:</property>
+                    <property name="xalign">1</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label" translatable="yes">End:</property>
+                    <property name="xalign">1</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton">
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSpinButton">
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkStack">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <child>
+                  <!-- n-columns=2 n-rows=3 -->
+                  <object class="GtkGrid">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="row-spacing">8</property>
+                    <property name="column-spacing">8</property>
+                    <child>
+                      <object class="GtkSpinButton" id="shannon_step">
+                        <property name="visible">True</property>
+                        <property name="can-focus">True</property>
+                      </object>
+                      <packing>
+                        <property name="left-attach">1</property>
+                        <property name="top-attach">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkColorButton" id="shannon_color">
+                        <property name="visible">True</property>
+                        <property name="can-focus">True</property>
+                        <property name="receives-default">True</property>
+                        <property name="rgba">rgb(52,101,164)</property>
+                      </object>
+                      <packing>
+                        <property name="left-attach">1</property>
+                        <property name="top-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="orientation">vertical</property>
+                      </object>
+                      <packing>
+                        <property name="left-attach">0</property>
+                        <property name="top-attach">0</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="label" translatable="yes">Color:</property>
+                        <property name="xalign">1</property>
+                      </object>
+                      <packing>
+                        <property name="left-attach">0</property>
+                        <property name="top-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="label" translatable="yes">Step:</property>
+                        <property name="xalign">1</property>
+                      </object>
+                      <packing>
+                        <property name="left-attach">0</property>
+                        <property name="top-attach">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">page0</property>
+                    <property name="title" translatable="yes">page0</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <!-- n-columns=2 n-rows=2 -->
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <child>
+          <object class="GtkLabel" id="h_legend">
+            <property name="visible">True</property>
+            <property name="can-focus">False</property>
+            <property name="margin-end">8</property>
+            <property name="margin-bottom">8</property>
+            <property name="label" translatable="yes">label</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="v_legend">
+            <property name="visible">True</property>
+            <property name="can-focus">False</property>
+            <property name="margin-start">8</property>
+            <property name="margin-top">8</property>
+            <property name="label" translatable="yes">label</property>
+            <property name="angle">90</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="visible">True</property>
+            <property name="can-focus">False</property>
+            <child>
+              <object class="GtkDrawingArea" id="content">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="margin-start">4</property>
+                <property name="margin-end">8</property>
+                <property name="margin-top">8</property>
+                <property name="margin-bottom">4</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <signal name="draw" handler="_render_content_glimpse" swapped="no"/>
+              </object>
+              <packing>
+                <property name="name">page0</property>
+                <property name="title" translatable="yes">page0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSpinner" id="mask">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="active">True</property>
+              </object>
+              <packing>
+                <property name="name">page1</property>
+                <property name="title" translatable="yes">page1</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
-- 
cgit v0.11.2-87-g4458