diff options
author | Cyrille Bagard <nocbos@gmail.com> | 2020-12-31 11:42:08 (GMT) |
---|---|---|
committer | Cyrille Bagard <nocbos@gmail.com> | 2020-12-31 12:39:08 (GMT) |
commit | 406a8a3c8eab4691ff32271ca906e93556d845dc (patch) | |
tree | 7bb5d771804c0a006ab8a1f474750a34e48cd018 | |
parent | 8965133f188c817cbdf4fcf9d1f1b60462bbfe7d (diff) |
Completed the Python plugin displaying file entropy.
-rw-r--r-- | plugins/python/cglimpse/core.py | 19 | ||||
-rw-r--r-- | plugins/python/cglimpse/distro.py | 61 | ||||
-rw-r--r-- | plugins/python/cglimpse/entropy.py | 137 | ||||
-rw-r--r-- | plugins/python/cglimpse/method.py | 10 | ||||
-rw-r--r-- | plugins/python/cglimpse/panel.py | 130 | ||||
-rw-r--r-- | plugins/python/cglimpse/panel.ui | 140 |
6 files changed, 380 insertions, 117 deletions
diff --git a/plugins/python/cglimpse/core.py b/plugins/python/cglimpse/core.py index d2cfdc4..29fb535 100644 --- a/plugins/python/cglimpse/core.py +++ b/plugins/python/cglimpse/core.py @@ -1,5 +1,6 @@ from pychrysalide import PluginModule +from pychrysalide.glibext import ConfigParam from pychrysalide.gui import core from .panel import CGlimpsePanel @@ -13,7 +14,7 @@ class ContentGlimpse(PluginModule): _version = '0.1' _url = 'https://www.chrysalide.re/' - _actions = ( ) + _actions = ( PluginModule.PluginAction.PLUGIN_LOADED, PluginModule.PluginAction.PANEL_CREATION, ) def __init__(self): @@ -22,3 +23,19 @@ class ContentGlimpse(PluginModule): super(ContentGlimpse, self).__init__() core.register_panel(CGlimpsePanel) + + + def _manage(self, action): + """React to several steps of the plugin life.""" + + CGlimpsePanel.setup_config(self.config) + + return True + + + def _on_panel_creation(self, action, item): + """Get notified of a new panel creation.""" + + if type(item) == CGlimpsePanel: + + item.attach_config(self.config) diff --git a/plugins/python/cglimpse/distro.py b/plugins/python/cglimpse/distro.py index 5ffc523..4b490e6 100644 --- a/plugins/python/cglimpse/distro.py +++ b/plugins/python/cglimpse/distro.py @@ -1,20 +1,28 @@ +from gi.repository import Gdk +from pychrysalide.glibext import ConfigParam + from .method import GlimpseMethod class ByteDistribution(GlimpseMethod): - def __init__(self, builder): - """Prepare a Shannon entropy display.""" - super(ByteDistribution, self).__init__(builder) + @staticmethod + def setup_config(config): + """Register the configuration parameters for the method.""" - button = builder.get_object('shannon_color') - button.connect('color-set', self._on_color_set) + color = Gdk.RGBA() + color.parse('#3465A4') + + param = ConfigParam('cglimpse.distrib.color', ConfigParam.ConfigParamType.COLOR, color) + config.add(param) - self._on_color_set(button) - self._step = 0x80 + def __init__(self, builder, config, update_cb): + """Prepare a Distrib entropy display.""" + + super(ByteDistribution, self).__init__(builder, config, update_cb) self._v_legend = 'Quantity' self._h_legend = 'Byte values' @@ -24,41 +32,42 @@ class ByteDistribution(GlimpseMethod): self._values = {} + button = builder.get_object('distrib_color') + button.connect('color-set', self._on_color_set) - def _on_color_set(self, button): - """React on color chosen for the rendering.""" + param = config.search('cglimpse.distrib.color') + color = param.value - 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 ] + button.set_rgba(param.value) - def format_legend(self, value, vert): - """Build the label used for a rule.""" - if vert: - text = str(value) + def _on_color_set(self, button): + """React on color chosen for the rendering.""" - else: + color = button.get_rgba() - scale = [ ' kb', ' Mb', ' Gb', ' Tb' ] - suffix = '' + 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 ] + + param = self._config.search('cglimpse.distrib.color') + param.value = color - for i in range(len(scale)): + self._update_cb() - if value < 1024: - break - value /= 1024 - suffix = scale[i] + def format_legend(self, value, vert): + """Build the label used for a rule.""" - text = '%u%s' % (value, suffix) + text = str(int(value)) return text - def update(self, data): - """Provide a description for the method.""" + def update(self, data, coverage): + """Compute internal values for the method.""" max_count = 0 @@ -67,7 +76,7 @@ class ByteDistribution(GlimpseMethod): for i in range(256): self._values[i] = 0 - for b in data: + for b in data[coverage[0] : coverage[1]]: if b in self._values.keys(): self._values[b] += 1 diff --git a/plugins/python/cglimpse/entropy.py b/plugins/python/cglimpse/entropy.py index f090624..5c4df5a 100644 --- a/plugins/python/cglimpse/entropy.py +++ b/plugins/python/cglimpse/entropy.py @@ -1,22 +1,32 @@ import math +from gi.repository import Gdk +from pychrysalide.glibext import ConfigParam from .method import GlimpseMethod class ShannonEntropy(GlimpseMethod): - def __init__(self, builder): - """Prepare a Shannon entropy display.""" - super(ShannonEntropy, self).__init__(builder) + @staticmethod + def setup_config(config): + """Register the configuration parameters for the method.""" - button = builder.get_object('shannon_color') - button.connect('color-set', self._on_color_set) + color = Gdk.RGBA() + color.parse('#3465A4') - self._on_color_set(button) + param = ConfigParam('cglimpse.shannon.color', ConfigParam.ConfigParamType.COLOR, color) + config.add(param) - self._step = 0x80 + param = ConfigParam('cglimpse.shannon.step', ConfigParam.ConfigParamType.ULONG, 0x80) + config.add(param) + + + def __init__(self, builder, config, update_cb): + """Prepare a Shannon entropy display.""" + + super(ShannonEntropy, self).__init__(builder, config, update_cb) self._v_legend = 'Entropy' self._h_legend = 'Byte offsets' @@ -24,17 +34,64 @@ class ShannonEntropy(GlimpseMethod): self._x_range = [ 0, 1024, 10240 ] self._y_range = [ 0.0, 0.25, 1.0 ] - self._size = None + self._coverage = None self._values = [] + button = builder.get_object('shannon_color') + button.connect('color-set', self._on_color_set) + + param = config.search('cglimpse.shannon.color') + color = param.value + + 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 ] + + button.set_rgba(param.value) + + scale = builder.get_object('shannon_step') + scale.connect('format-value', self._on_scale_format_value) + scale.connect('value-changed', self._on_scale_value_changed) + + param = config.search('cglimpse.shannon.step') + step = param.value + + self._step = step + + scale.set_value(step) + 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 ] + param = self._config.search('cglimpse.shannon.color') + param.value = color + + self._update_cb() + + + def _on_scale_value_changed(self, scale): + """React when the range value changes.""" + + step = int(scale.get_value()) + + self._step = step + + param = self._config.search('cglimpse.shannon.step') + param.value = step + + self._update_cb() + + + def _on_scale_format_value(self, scale, value): + """Change how the scale value is displayed.""" + + return '0x%x' % int(value) + def format_legend(self, value, vert): """Build the label used for a rule.""" @@ -44,42 +101,53 @@ class ShannonEntropy(GlimpseMethod): else: + multi = 1 + scale = [ ' kb', ' Mb', ' Gb', ' Tb' ] suffix = '' + remaining = value + for i in range(len(scale)): - if value < 1024: + if remaining < 1024: break - value /= 1024 + multi *= 1024 + + remaining /= 1024 suffix = scale[i] - text = '%u%s' % (value, suffix) + if value % multi == 0: + text = '%u%s' % (remaining, suffix) + else: + text = '%.1f%s' % (value / multi, suffix) return text - def update(self, data): - """Provide a description for the method.""" + def update(self, data, coverage): + """Compute internal values for the method.""" - self._size = len(data) + self._coverage = coverage - step = 2 ** math.ceil(math.log(self._size / 10, 2)) + size = self._coverage[1] - self._coverage[0] - self._x_range = [ 0, step, 10 * step ] + step = math.ceil(size / 10) + + self._x_range = [ coverage[0], step, coverage[0] + 10 * step ] self._values = [] - for i in range(0, self._size, self._step): + for i in range(self._x_range[0], self._x_range[2], self._step): counter = [ 0 for i in range(256) ] start = i end = i + self._step - if end > self._size: - end = self._size + if end > self._coverage[1]: + end = self._coverage[1] for b in data[start : end]: counter[b] += 1 @@ -97,31 +165,29 @@ class ShannonEntropy(GlimpseMethod): 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)) + size = self._coverage[1] - self._coverage[0] - if self._size % step == 0: - full_size = self._size - else: - full_size = (self._size + step - 1) & ~(step - 1) + step = math.ceil(size / 10) - start = 0 + start = self._coverage[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): + for i in range(self._x_range[0], self._x_range[2], self._step): end = i + self._step - if end > self._size: - end = self._size + if end > self._coverage[1]: + end = self._coverage[1] - x = area[0] + ((end - start) * area[2]) / full_size - y = area[1] + area[3] - (area[3] * self._values[int(i / self._step)]) + x = area[0] + ((end - start) * area[2]) / (10 * step) + y = area[1] + area[3] - (area[3] * self._values[int((i - self._x_range[0]) / self._step)]) if last_y != y: cr.line_to(last_x, y + 2) + last_y = y cr.line_to(x, y + 2) @@ -136,18 +202,19 @@ class ShannonEntropy(GlimpseMethod): 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): + for i in range(self._x_range[0], self._x_range[2], self._step): end = i + self._step - if end > self._size: - end = self._size + if end > self._coverage[1]: + end = self._coverage[1] - x = area[0] + ((end - start) * area[2]) / full_size - y = area[1] + area[3] - (area[3] * self._values[int(i / self._step)]) + x = area[0] + ((end - start) * area[2]) / (10 * step) + y = area[1] + area[3] - (area[3] * self._values[int((i - self._x_range[0]) / self._step)]) if last_y != y: cr.line_to(last_x, y) + last_y = y cr.line_to(x, y) diff --git a/plugins/python/cglimpse/method.py b/plugins/python/cglimpse/method.py index f2e9fdb..731271e 100644 --- a/plugins/python/cglimpse/method.py +++ b/plugins/python/cglimpse/method.py @@ -3,10 +3,12 @@ class GlimpseMethod(): """Abstract class for gimpses.""" - def __init__(self, builder): + def __init__(self, builder, config, update_cb): """Populate the class with its attributes.""" self._builder = builder + self._config = config + self._update_cb = update_cb self._v_legend = None self._h_legend = None @@ -37,6 +39,12 @@ class GlimpseMethod(): return str(value) + def update(self, data, coverage): + """Compute internal values for the method.""" + + pass + + def render(self, cr, area): """Draw the bytes distribution for the current binary, if any.""" diff --git a/plugins/python/cglimpse/panel.py b/plugins/python/cglimpse/panel.py index 2245e3a..a195b0d 100644 --- a/plugins/python/cglimpse/panel.py +++ b/plugins/python/cglimpse/panel.py @@ -2,6 +2,7 @@ import os from gi.repository import Gtk from pychrysalide import core +from pychrysalide.glibext import ConfigParam from pychrysalide.gtkext import BuiltNamedWidget from pychrysalide.gtkext import EasyGtk from pychrysalide.gui import PanelItem @@ -21,6 +22,18 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): _working_group_id = core.setup_tiny_global_work_group() + @staticmethod + def setup_config(config): + """Register the configuration parameters for all the methods.""" + + param = ConfigParam('cglimpse.selected', ConfigParam.ConfigParamType.ULONG, 0) + config.add(param) + + ShannonEntropy.setup_config(config) + + ByteDistribution.setup_config(config) + + def __init__(self): """Initialize the GUI panel.""" @@ -35,10 +48,35 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): self._builder.connect_signals(self) + self._start_changed_sid = -1 + self._end_changed_sid = -1 + self._content = None + self._start = 0 + self._end = 0 + + self._config = None + self._methods = {} + self._current = None + + + def attach_config(self, config): + """Attach a loaded configuration to the displayed panel.""" + + self._config = config + + self._methods['shannon'] = ShannonEntropy(self._builder, config, self._force_update) + self._methods['distrib'] = ByteDistribution(self._builder, config, self._force_update) + + param = config.search('cglimpse.selected') + selected = param.value combo = self._builder.get_object('method_sel') - self._on_method_changed(combo) + + if selected == combo.get_active(): + self._on_method_changed(combo) + else: + combo.set_active(selected) def _change_content(self, old, new): @@ -46,6 +84,23 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): self._content = new.content if new else None + length = len(self._content.data) if self._content else 1 + + self._start = 0 + self._end = length + + scale = self._builder.get_object('start_pos') + scale.set_range(0, length - 1) + scale.set_value(0) + + self._start_changed_sid = scale.connect('value-changed', self._on_position_value_changed) + + scale = self._builder.get_object('end_pos') + scale.set_range(1, length) + scale.set_value(length) + + self._end_changed_sid = scale.connect('value-changed', self._on_position_value_changed) + combo = self._builder.get_object('method_sel') self._on_method_changed(combo) @@ -71,7 +126,7 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): assert(uid == 0) - self._current.update(self._content.data) + self._current.update(self._content.data, [ self._start, self._end ]) area = self._builder.get_object('content') area.queue_draw() @@ -93,6 +148,28 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): assert(uid == 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 _on_method_changed(self, combo): """React on method selection change.""" @@ -103,40 +180,49 @@ class CGlimpsePanel(PanelItem, UpdatablePanel): 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) + assert(key in self._methods.keys()) + self._current = self._methods[key] self._current.switch() - if self._content: + stack = self._builder.get_object('specific_options') + stack.set_visible_child_name('%s_page' % key) - self.run_update(0) + param = self._config.search('cglimpse.selected') + param.value = combo.get_active() + self._force_update() - def _on_options_toggled(self, button): - """React on options display/hide order.""" - lbl = self._builder.get_object('options_label') + def _on_position_value_changed(self, scale): + """React when the data range value changes.""" - common = self._builder.get_object('common_options') + start_scale = self._builder.get_object('start_pos') + self._start = int(start_scale.get_value()) - if button.get_active(): + end_scale = self._builder.get_object('end_pos') + self._end = int(end_scale.get_value()) - button.get_parent().child_set_property(button, 'expand', False) - lbl.set_angle(0) + length = len(self._content.data) if self._content else 1 - common.show() + start_scale.disconnect(self._start_changed_sid) + end_scale.disconnect(self._end_changed_sid) - else: + start_scale.set_range(0, self._end - 1) + end_scale.set_range(self._start + 1, length) - button.get_parent().child_set_property(button, 'expand', True) - lbl.set_angle(90) + self._start_changed_sid = start_scale.connect('value-changed', self._on_position_value_changed) + self._end_changed_sid = end_scale.connect('value-changed', self._on_position_value_changed) - common.hide() + self._force_update() + + + def _force_update(self): + """Force the update of the rendering.""" + + if self._content: + + self.run_update(0) def _render_grid(self, widget, cr): diff --git a/plugins/python/cglimpse/panel.ui b/plugins/python/cglimpse/panel.ui index 0ec6dc5..8299484 100644 --- a/plugins/python/cglimpse/panel.ui +++ b/plugins/python/cglimpse/panel.ui @@ -2,6 +2,22 @@ <!-- Generated with glade 3.37.0 --> <interface> <requires lib="gtk+" version="3.24"/> + <object class="GtkAdjustment" id="end_pos_adj"> + <property name="upper">100</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + </object> + <object class="GtkAdjustment" id="shannon_step_adj"> + <property name="lower">16</property> + <property name="upper">1024</property> + <property name="value">128</property> + <property name="step-increment">1</property> + </object> + <object class="GtkAdjustment" id="start_pos_adj"> + <property name="upper">100</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + </object> <object class="GtkBox" id="box"> <property name="visible">True</property> <property name="can-focus">False</property> @@ -38,22 +54,24 @@ <property name="margin-end">8</property> <property name="margin-top">8</property> <property name="margin-bottom">8</property> + <property name="hexpand">False</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="hexpand">True</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="hexpand">True</property> <property name="active">0</property> <items> - <item id="shanon" translatable="yes">Shanon entropy</item> + <item id="shannon" translatable="yes">Shannon entropy</item> <item id="distrib" translatable="yes">Bytes distribution</item> </items> <signal name="changed" handler="_on_method_changed" swapped="no"/> @@ -89,9 +107,16 @@ </packing> </child> <child> - <object class="GtkSpinButton"> + <object class="GtkScale" id="start_pos"> + <property name="width-request">200</property> <property name="visible">True</property> <property name="can-focus">True</property> + <property name="can-default">True</property> + <property name="adjustment">start_pos_adj</property> + <property name="show-fill-level">True</property> + <property name="round-digits">0</property> + <property name="digits">0</property> + <property name="value-pos">right</property> </object> <packing> <property name="left-attach">1</property> @@ -99,9 +124,16 @@ </packing> </child> <child> - <object class="GtkSpinButton"> + <object class="GtkScale" id="end_pos"> + <property name="width-request">200</property> <property name="visible">True</property> <property name="can-focus">True</property> + <property name="can-default">True</property> + <property name="adjustment">end_pos_adj</property> + <property name="show-fill-level">True</property> + <property name="round-digits">0</property> + <property name="digits">0</property> + <property name="value-pos">right</property> </object> <packing> <property name="left-attach">1</property> @@ -116,28 +148,30 @@ </packing> </child> <child> - <object class="GtkStack"> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkStack" id="specific_options"> <property name="visible">True</property> <property name="can-focus">False</property> + <property name="hexpand">True</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="width-request">200</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> @@ -145,26 +179,26 @@ </object> <packing> <property name="left-attach">1</property> - <property name="top-attach">1</property> + <property name="top-attach">0</property> </packing> </child> <child> - <object class="GtkSeparator"> + <object class="GtkLabel"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="orientation">vertical</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">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="label" translatable="yes">Step:</property> <property name="xalign">1</property> </object> <packing> @@ -173,28 +207,67 @@ </packing> </child> <child> + <object class="GtkScale" id="shannon_step"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="can-default">True</property> + <property name="adjustment">shannon_step_adj</property> + <property name="show-fill-level">True</property> + <property name="digits">0</property> + <property name="value-pos">right</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">shannon_page</property> + </packing> + </child> + <child> + <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="GtkColorButton" id="distrib_color"> + <property name="width-request">200</property> + <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">0</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="label" translatable="yes">Color:</property> <property name="xalign">1</property> </object> <packing> <property name="left-attach">0</property> - <property name="top-attach">2</property> + <property name="top-attach">0</property> </packing> </child> </object> <packing> - <property name="name">page0</property> - <property name="title" translatable="yes">page0</property> + <property name="name">distrib_page</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> + <property name="position">2</property> </packing> </child> </object> @@ -204,9 +277,6 @@ <property name="position">1</property> </packing> </child> - <child> - <placeholder/> - </child> </object> <packing> <property name="expand">False</property> @@ -215,7 +285,6 @@ </packing> </child> <child> - <!-- n-columns=2 n-rows=2 --> <object class="GtkGrid"> <property name="visible">True</property> <property name="can-focus">False</property> @@ -254,9 +323,9 @@ <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-start">8</property> <property name="margin-end">8</property> - <property name="margin-top">8</property> + <property name="margin-top">4</property> <property name="margin-bottom">4</property> <property name="hexpand">True</property> <property name="vexpand">True</property> @@ -271,6 +340,12 @@ <object class="GtkSpinner" id="mask"> <property name="visible">True</property> <property name="can-focus">False</property> + <property name="margin-start">8</property> + <property name="margin-end">8</property> + <property name="margin-top">4</property> + <property name="margin-bottom">4</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> <property name="active">True</property> </object> <packing> @@ -292,6 +367,7 @@ <packing> <property name="expand">True</property> <property name="fill">True</property> + <property name="pack-type">end</property> <property name="position">1</property> </packing> </child> |