diff options
Diffstat (limited to 'plugins/python')
| -rw-r--r-- | plugins/python/Makefile.am | 1 | ||||
| -rw-r--r-- | plugins/python/cglimpse/Makefile.am | 10 | ||||
| -rw-r--r-- | plugins/python/cglimpse/__init__.py | 2 | ||||
| -rw-r--r-- | plugins/python/cglimpse/core.py | 24 | ||||
| -rw-r--r-- | plugins/python/cglimpse/distro.py | 101 | ||||
| -rw-r--r-- | plugins/python/cglimpse/entropy.py | 158 | ||||
| -rw-r--r-- | plugins/python/cglimpse/method.py | 43 | ||||
| -rw-r--r-- | plugins/python/cglimpse/panel.py | 323 | ||||
| -rw-r--r-- | plugins/python/cglimpse/panel.ui | 299 | 
9 files changed, 961 insertions, 0 deletions
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>  | 
