diff --git a/GUI/IPconvGUI.py b/GUI/IPconvGUI.py
new file mode 100644
index 0000000..d4cf050
--- /dev/null
+++ b/GUI/IPconvGUI.py
@@ -0,0 +1,322 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+import wx
+import wx.adv
+import wx.lib.agw.persist as pm
+from .IPconvGUIbase import BaseConverterFrame, BaseSettingsFrame
+from .resources import *
+
+
+# ctrl-[A,C,V,X,Z], shift-insert
+ctrl_ascii_chars = [
+ wx.WXK_CONTROL_A, wx.WXK_CONTROL_C, wx.WXK_CONTROL_V, wx.WXK_CONTROL_X, wx.WXK_CONTROL_Z, wx.WXK_INSERT
+]
+
+
+class IPConverterFrame(BaseConverterFrame):
+ def __init__(self, *args, **kwds):
+ BaseConverterFrame.__init__(self, *args, **kwds)
+ # Used for dragging implementation
+ self._initial_position = None
+
+ # Last selected text control is remembered for use with reverse checkbox callback
+ self.last_selected = self.text_ctrl_hex
+
+ self.settings_window = SettingsFrame(self, name='SettingsFrame')
+
+ self.themes = {
+ 'dark': {
+ 'background': {'main': wx.Colour(60, 60, 60), 'text': wx.Colour(85, 85, 85)},
+ 'foreground': {'main': wx.Colour(192, 192, 192), 'text': wx.Colour(232, 232, 232)}
+ },
+ 'light': {
+ 'background': {'main': wx.Colour(238, 238, 238), 'text': wx.Colour(255, 255, 255)},
+ 'foreground': {'main': wx.Colour(0, 0, 0), 'text': wx.Colour(0, 0, 0)}
+ }
+ }
+
+ # Hotkey registration and binding
+ # TODO: Check the return from RegisterHotKey
+ # TODO: Identify why Win+Z wasn't working for me on Windows 10 (Ctrl-Win-Z works)
+ self.RegisterHotKey(self.text_ctrl_hex.GetId(), wx.MOD_CONTROL | wx.MOD_META, ord('Z')) # Ctrl-Win-Z
+ self.Bind(wx.EVT_HOTKEY, self.on_paste, id=self.text_ctrl_hex.GetId())
+
+ # Catching when the window transitions to/from being the active window the user is interacting with
+ self.Bind(wx.EVT_ACTIVATE, self.on_activate)
+
+ # Allow general handling of focus events for any relevant widget
+ self.Bind(wx.EVT_CHILD_FOCUS, self.on_child_focus)
+
+ # Allow 'minimizing' the window when pressing Escape if the frame has focus
+ self.panel_main.Bind(wx.EVT_CHAR_HOOK, self.on_key)
+
+ # Mouse bindings (for dragging the panel)
+ self.panel_main.Bind(wx.EVT_LEFT_UP, self.on_mouse_button_up)
+ self.panel_main.Bind(wx.EVT_MOTION, self.on_mouse)
+ self.panel_main.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.on_mouse_lost)
+
+ # Catching text input, used to automatically run conversions
+ self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_dec)
+ self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_dotted)
+ self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_hex)
+
+ # Catching individual character input, to only allow valid characters
+ self.text_ctrl_dec.Bind(wx.EVT_CHAR, self.on_char)
+ self.text_ctrl_dotted.Bind(wx.EVT_CHAR, self.on_char)
+ self.text_ctrl_hex.Bind(wx.EVT_CHAR, self.on_char)
+
+ self.text_ctrl_dec.Bind(wx.EVT_TEXT_PASTE, self.on_paste)
+ self.text_ctrl_dotted.Bind(wx.EVT_TEXT_PASTE, self.on_paste)
+ self.text_ctrl_hex.Bind(wx.EVT_TEXT_PASTE, self.on_paste)
+
+ self.SetName('MainFrame')
+ self.checkbox_reverse.SetName('checkbox_reverse')
+
+ # TODO: Confirm behavior with multiple monitors, especially on non-Windows systems.
+ self.Center()
+
+ self.persistence_manager = pm.PersistenceManager.Get()
+ self.persistence_manager.SetManagerStyle(pm.PM_DEFAULT_STYLE | pm.PM_PERSIST_CONTROL_VALUE)
+
+ # Restore any saved selections and window placement
+ self.persistence_manager.RegisterAndRestore(self)
+ self.persistence_manager.RegisterAndRestoreAll(
+ self, children=[
+ self.checkbox_reverse, self.settings_window.checkbox_monitorclipboard,
+ self.settings_window.checkbox_stayontop, self.settings_window.radio_box_theme
+ ]
+ )
+
+ self.apply_theme(theme_name=self.settings_window.radio_box_theme.GetStringSelection())
+ self.monitor_clipboard_start()
+ self.stay_on_top(enable=self.settings_window.checkbox_stayontop.IsChecked())
+
+ def apply_theme(self, theme_name: str):
+ theme = self.themes.get(theme_name, None)
+ if not theme:
+ return # TODO: consider raising an exception, especially if customization is introduced.
+
+ # Main colors
+ background_main = theme['background']['main']
+ foreground_main = theme['foreground']['main']
+
+ # Colors for TextCtrl objects
+ background_text = theme['background']['text']
+ foreground_text = theme['foreground']['text']
+
+ self.checkbox_reverse.SetBackgroundColour(background_main)
+ self.checkbox_reverse.SetForegroundColour(foreground_main)
+
+ self.bitmap_button_settings.SetBackgroundColour(background_main)
+ self.bitmap_button_exit.SetBackgroundColour(background_main)
+
+ self.text_ctrl_dotted.SetBackgroundColour(background_text)
+ self.text_ctrl_dotted.SetForegroundColour(foreground_text)
+ self.text_ctrl_hex.SetBackgroundColour(background_text)
+ self.text_ctrl_hex.SetForegroundColour(foreground_text)
+ self.text_ctrl_dec.SetBackgroundColour(background_text)
+ self.text_ctrl_dec.SetForegroundColour(foreground_text)
+
+ self.label_dotted.SetBackgroundColour(background_main)
+ self.label_dotted.SetForegroundColour(foreground_main)
+ self.label_hex.SetBackgroundColour(background_main)
+ self.label_hex.SetForegroundColour(foreground_main)
+ self.label_dec.SetBackgroundColour(background_main)
+ self.label_dec.SetForegroundColour(foreground_main)
+
+ self.panel_main.SetBackgroundColour(background_main)
+ self.panel_main.SetForegroundColour(foreground_main)
+
+ self.Refresh()
+
+ @staticmethod
+ def get_clipboard_string() -> tuple:
+ """Try to get text data from the clipboard and return a tuple indicating success and the text value"""
+ clipboard_string = ''
+ success = False
+ text_data = wx.TextDataObject()
+ if wx.TheClipboard.Open():
+ success = wx.TheClipboard.GetData(text_data)
+ wx.TheClipboard.Close()
+ if success:
+ clipboard_string = text_data.GetText()
+ return success, clipboard_string
+
+ def monitor_clipboard(self):
+ """Monitor the clipboard when this application does not have focus"""
+ if self.settings_window.checkbox_monitorclipboard.IsChecked() and (not self.IsActive()):
+ # Submit another run to start later
+ self.monitor_clipboard_start()
+ # Return the data, which should be used in a subclass
+ return self.get_clipboard_string()
+ else:
+ return None, None
+
+ def monitor_clipboard_start(self):
+ """Submit a delayed call for monitoring. This is useful to allow event handlers to complete beforehand."""
+ wx.CallLater(1000, self.monitor_clipboard)
+
+ def on_activate(self, event):
+ """This is triggered when the user changes focus between this application and others"""
+ # Check for valid self in case window has been destroyed (avoids exception when closing the program)
+ if self and self.settings_window:
+ # Start monitoring clipboard if we're configured to and another application has focus
+ if self.settings_window.checkbox_monitorclipboard.IsChecked() and (not event.GetActive()):
+ self.monitor_clipboard_start()
+ event.Skip()
+
+ def on_button_exit(self, event):
+ """Save window position and settings when the user presses the button to close the application"""
+ self.persistence_manager.SaveAndUnregister()
+ self.Close(force=True)
+
+ def on_button_settings(self, event):
+ """Open the settings window, or focus it if it's already open."""
+ if self.settings_window.IsShown():
+ self.settings_window.Raise()
+ else:
+ self.settings_window.Show()
+ self.settings_window.CenterOnParent()
+ self.settings_window.checkbox_monitorclipboard.SetFocus()
+
+ def on_char(self, event):
+ """Allow movement and command keys in TextCtrl objects"""
+ if event.IsKeyInCategory(wx.WXK_CATEGORY_NAVIGATION | wx.WXK_CATEGORY_CUT | wx.WXK_CATEGORY_TAB):
+ event.Skip()
+ elif event.GetKeyCode() in ctrl_ascii_chars:
+ event.Skip()
+
+ def on_checkbox_reverse(self, event):
+ print("Event handler 'on_checkbox_reverse' not implemented!")
+ event.Skip()
+
+ def on_child_focus(self, event):
+ """Allows general handling of focus events for different types of widgets throughout the program"""
+ event_object = event.GetEventObject()
+ if isinstance(event_object, wx.TextCtrl):
+ if event_object in [self.text_ctrl_dotted, self.text_ctrl_hex, self.text_ctrl_dec]:
+ self.last_selected = event_object
+ wx.CallAfter(event_object.SetInsertionPointEnd)
+ wx.CallAfter(event_object.SelectAll)
+ else:
+ event.Skip()
+
+ def on_key(self, event):
+ """Minimize (iconize) the window on escape key"""
+ if event.GetKeyCode() == wx.WXK_ESCAPE:
+ self.Iconize()
+ else:
+ event.Skip()
+
+ def on_mouse(self, event):
+ """Implement click-and-drag for the frame/panel"""
+ if not event.Dragging():
+ # Panel is not being dragged, reset and bail
+ self._initial_position = None
+ return
+ if not self.panel_main.HasCapture():
+ # In Drag event, make sure we capture the mouse
+ self.panel_main.CaptureMouse()
+ if not self._initial_position:
+ # Panel is being dragged, store current position
+ self._initial_position = event.GetPosition()
+ else:
+ # Panel is being dragged and we already have a previous position, move the window
+ new_position = event.GetPosition()
+ delta = self._initial_position - new_position
+ self.SetPosition(self.GetPosition() - delta)
+
+ def on_mouse_button_up(self, event):
+ """Makes sure the mouse gets released from dragging"""
+ if self.panel_main.HasCapture():
+ self.panel_main.ReleaseMouse()
+
+ def on_mouse_lost(self, event):
+ """This function can be used to abort anything relying on mouse input."""
+ pass
+
+ def on_paste(self, event) -> tuple:
+ """This will get the value from the clipboard and return a success indicator and string in a tuple"""
+ return self.get_clipboard_string()
+
+ def on_text(self, event):
+ print("Event handler 'on_text' not implemented!")
+ event.Skip()
+
+ def stay_on_top(self, enable: bool = True):
+ if enable:
+ # Binary OR wx.STAY_ON_TOP to add it if it's not already present
+ self.SetWindowStyle(self.GetWindowStyle() | wx.STAY_ON_TOP)
+ else:
+ # Binary XOR wx.STAY_ON_TOP to remove it if it is present
+ self.SetWindowStyle(self.GetWindowStyle() ^ wx.STAY_ON_TOP)
+
+
+class SettingsFrame(BaseSettingsFrame):
+ def __init__(self, *args, **kwds):
+ kwds["style"] = kwds.get("style", 0)
+ BaseSettingsFrame.__init__(self, *args, **kwds)
+ self.Bind(wx.EVT_CLOSE, self.on_close)
+
+ self.checkbox_monitorclipboard.SetName('checkbox_monitorclipboard')
+ self.checkbox_stayontop.SetName('checkbox_stayontop')
+ self.radio_box_theme.SetName('radio_box_theme')
+
+ def on_about(self, event):
+ program_description = \
+ """Quick IP Converter can convert IPv4 addresses between dotted-quad, decimal and hexadecimal notation."""
+ program_license = \
+ "LGPL-3.0-or-later (GNU Lesser General Public License 3.0 or later)\n" \
+ "See the files COPYING and COPYING.LESSER distributed with this program\n" \
+ "or https://www.gnu.org/licenses/ if you did not receive them with your copy."
+ info = wx.adv.AboutDialogInfo()
+ info.SetIcon(IPconvPNG.GetIcon())
+ info.SetName('Quick IP Converter')
+ info.SetVersion('2.0')
+ info.SetDescription(program_description)
+ info.SetCopyright('(C) 2014, 2018 Brandon M. Pace ')
+ info.SetWebSite('https://github.com/brandonmpace/Quick-IP-Converter')
+ info.SetLicense(program_license)
+ # info.AddDeveloper('Brandon M. Pace')
+
+ wx.adv.AboutBox(info, parent=self)
+
+ def on_checkbox_monitorclipboard(self, event):
+ if event.IsChecked():
+ self.Parent.monitor_clipboard_start()
+ event.Skip()
+
+ def on_checkbox_stayontop(self, event):
+ self.Parent.stay_on_top(event.IsChecked())
+ event.Skip()
+
+ def on_close(self, event):
+ """This function is used to just hide the settings window when the user clicks the close button."""
+ if event.CanVeto():
+ self.Hide()
+ event.Veto()
+ else:
+ event.Skip()
+
+ def on_radiobox_theme(self, event):
+ self.Parent.apply_theme(event.GetString())
+ event.Skip()
diff --git a/GUI/IPconvGUIbase.py b/GUI/IPconvGUIbase.py
new file mode 100644
index 0000000..6aba2f8
--- /dev/null
+++ b/GUI/IPconvGUIbase.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+#
+# generated by wxGlade 0.9.0b1 on Sun Dec 16 21:09:45 2018
+#
+
+import wx
+import wx.adv
+
+# begin wxGlade: dependencies
+# end wxGlade
+
+# begin wxGlade: extracode
+from .resources import *
+
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+# end wxGlade
+
+
+class BaseConverterFrame(wx.Frame):
+ def __init__(self, *args, **kwds):
+ # begin wxGlade: BaseConverterFrame.__init__
+ kwds["style"] = kwds.get("style", 0) | wx.CLIP_CHILDREN | wx.STAY_ON_TOP | wx.TAB_TRAVERSAL
+ wx.Frame.__init__(self, *args, **kwds)
+ self.panel_main = wx.Panel(self, wx.ID_ANY)
+ self.checkbox_reverse = wx.CheckBox(self.panel_main, wx.ID_ANY, "Reverse", style=wx.ALIGN_RIGHT)
+ self.bitmap_button_settings = wx.BitmapButton(self.panel_main, wx.ID_ANY, SettingsIcon.GetBitmap(), style=wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT)
+ self.bitmap_button_exit = wx.BitmapButton(self.panel_main, wx.ID_ANY, ExitIcon.GetBitmap(), style=wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT)
+ self.label_dotted = wx.StaticText(self.panel_main, wx.ID_ANY, "IP:", style=wx.ALIGN_RIGHT)
+ self.text_ctrl_dotted = wx.TextCtrl(self.panel_main, wx.ID_ANY, "")
+ self.label_hex = wx.StaticText(self.panel_main, wx.ID_ANY, "Hex:", style=wx.ALIGN_RIGHT)
+ self.text_ctrl_hex = wx.TextCtrl(self.panel_main, wx.ID_ANY, "")
+ self.label_dec = wx.StaticText(self.panel_main, wx.ID_ANY, "Dec:")
+ self.text_ctrl_dec = wx.TextCtrl(self.panel_main, wx.ID_ANY, "")
+
+ self.__set_properties()
+ self.__do_layout()
+
+ self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_reverse, self.checkbox_reverse)
+ self.Bind(wx.EVT_BUTTON, self.on_button_settings, self.bitmap_button_settings)
+ self.Bind(wx.EVT_BUTTON, self.on_button_exit, self.bitmap_button_exit)
+ # end wxGlade
+
+ def __set_properties(self):
+ # begin wxGlade: BaseConverterFrame.__set_properties
+ self.SetTitle("Quick IP Converter")
+ _icon = wx.NullIcon
+ _icon.CopyFromBitmap(IPconvIcon.GetBitmap())
+ self.SetIcon(_icon)
+ self.checkbox_reverse.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.checkbox_reverse.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.checkbox_reverse.SetToolTip("Reverse the byte order during conversion")
+ self.bitmap_button_settings.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.bitmap_button_settings.SetToolTip("Settings")
+ self.bitmap_button_settings.SetSize(self.bitmap_button_settings.GetBestSize())
+ self.bitmap_button_exit.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.bitmap_button_exit.SetToolTip("Exit")
+ self.bitmap_button_exit.SetSize(self.bitmap_button_exit.GetBestSize())
+ self.label_dotted.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.label_dotted.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_dotted.SetBackgroundColour(wx.Colour(255, 255, 255))
+ self.text_ctrl_dotted.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_dotted.SetToolTip("Dotted-quad IPv4 (e.g. 192.168.1.1)")
+ self.label_hex.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.label_hex.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_hex.SetBackgroundColour(wx.Colour(255, 255, 255))
+ self.text_ctrl_hex.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_hex.SetToolTip("Hexadecimal IP (e.g. c0a80101) or hexadecimal value")
+ self.text_ctrl_hex.SetFocus()
+ self.label_dec.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.label_dec.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_dec.SetBackgroundColour(wx.Colour(255, 255, 255))
+ self.text_ctrl_dec.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.text_ctrl_dec.SetToolTip("Decimal IP (e.g. 3232235777) or decimal value")
+ self.panel_main.SetBackgroundColour(wx.Colour(238, 238, 238))
+ self.panel_main.SetForegroundColour(wx.Colour(0, 0, 0))
+ self.panel_main.SetToolTip("Click and drag to move the window")
+ # end wxGlade
+
+ def __do_layout(self):
+ # begin wxGlade: BaseConverterFrame.__do_layout
+ sizer_main_outer = wx.BoxSizer(wx.VERTICAL)
+ grid_sizer_main = wx.GridBagSizer(0, 0)
+ sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
+ grid_sizer_main.Add(self.checkbox_reverse, (0, 0), (1, 2), wx.ALIGN_CENTER_VERTICAL, 0)
+ sizer_buttons.Add(self.bitmap_button_settings, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0)
+ sizer_buttons.Add(self.bitmap_button_exit, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.EXPAND | wx.LEFT, 2)
+ grid_sizer_main.Add(sizer_buttons, (0, 3), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.EXPAND, 0)
+ grid_sizer_main.Add(self.label_dotted, (1, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0)
+ grid_sizer_main.Add(self.text_ctrl_dotted, (1, 1), (1, 3), wx.EXPAND, 0)
+ grid_sizer_main.Add(self.label_hex, (2, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0)
+ grid_sizer_main.Add(self.text_ctrl_hex, (2, 1), (1, 3), wx.EXPAND, 0)
+ grid_sizer_main.Add(self.label_dec, (3, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0)
+ grid_sizer_main.Add(self.text_ctrl_dec, (3, 1), (1, 3), wx.EXPAND, 0)
+ self.panel_main.SetSizer(grid_sizer_main)
+ grid_sizer_main.AddGrowableCol(1)
+ sizer_main_outer.Add(self.panel_main, 1, wx.EXPAND, 0)
+ self.SetSizer(sizer_main_outer)
+ sizer_main_outer.Fit(self)
+ self.Layout()
+ # end wxGlade
+
+ def on_checkbox_reverse(self, event): # wxGlade: BaseConverterFrame.
+ print("Event handler 'on_checkbox_reverse' not implemented!")
+ event.Skip()
+
+ def on_button_settings(self, event): # wxGlade: BaseConverterFrame.
+ print("Event handler 'on_button_settings' not implemented!")
+ event.Skip()
+
+ def on_button_exit(self, event): # wxGlade: BaseConverterFrame.
+ print("Event handler 'on_button_exit' not implemented!")
+ event.Skip()
+
+# end of class BaseConverterFrame
+
+class BaseSettingsFrame(wx.Frame):
+ def __init__(self, *args, **kwds):
+ # begin wxGlade: BaseSettingsFrame.__init__
+ kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_TOOL_WINDOW | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL
+ wx.Frame.__init__(self, *args, **kwds)
+ self.panel_settings = wx.Panel(self, wx.ID_ANY)
+ self.checkbox_monitorclipboard = wx.CheckBox(self.panel_settings, wx.ID_ANY, "monitor clipboard")
+ self.checkbox_stayontop = wx.CheckBox(self.panel_settings, wx.ID_ANY, "stay on top")
+ self.radio_box_theme = wx.RadioBox(self.panel_settings, wx.ID_ANY, "theme", choices=["dark", "light"], majorDimension=0, style=wx.RA_SPECIFY_ROWS)
+ self.hyperlink_about = wx.adv.HyperlinkCtrl(self.panel_settings, wx.ID_ANY, "About this program", "", style=wx.adv.HL_ALIGN_CENTRE)
+
+ self.__set_properties()
+ self.__do_layout()
+
+ self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_monitorclipboard, self.checkbox_monitorclipboard)
+ self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_stayontop, self.checkbox_stayontop)
+ self.Bind(wx.EVT_RADIOBOX, self.on_radiobox_theme, self.radio_box_theme)
+ self.Bind(wx.adv.EVT_HYPERLINK, self.on_about, self.hyperlink_about)
+ # end wxGlade
+
+ def __set_properties(self):
+ # begin wxGlade: BaseSettingsFrame.__set_properties
+ self.SetTitle("Settings")
+ self.checkbox_monitorclipboard.SetToolTip("monitor clipboard every second for hex or dotted-quad values")
+ self.checkbox_monitorclipboard.SetFocus()
+ self.checkbox_stayontop.SetToolTip("always stay on top of other windows")
+ self.checkbox_stayontop.SetValue(1)
+ self.radio_box_theme.SetSelection(1)
+ # end wxGlade
+
+ def __do_layout(self):
+ # begin wxGlade: BaseSettingsFrame.__do_layout
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
+ sizer_panel = wx.BoxSizer(wx.VERTICAL)
+ sizer_panel.Add(self.checkbox_monitorclipboard, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
+ sizer_panel.Add(self.checkbox_stayontop, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
+ sizer_panel.Add(self.radio_box_theme, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 2)
+ sizer_panel.Add(self.hyperlink_about, 0, wx.ALIGN_CENTER | wx.ALL, 2)
+ self.panel_settings.SetSizer(sizer_panel)
+ sizer_main.Add(self.panel_settings, 1, wx.EXPAND, 0)
+ self.SetSizer(sizer_main)
+ sizer_main.Fit(self)
+ self.Layout()
+ # end wxGlade
+
+ def on_checkbox_monitorclipboard(self, event): # wxGlade: BaseSettingsFrame.
+ print("Event handler 'on_checkbox_monitorclipboard' not implemented!")
+ event.Skip()
+
+ def on_checkbox_stayontop(self, event): # wxGlade: BaseSettingsFrame.
+ print("Event handler 'on_checkbox_stayontop' not implemented!")
+ event.Skip()
+
+ def on_radiobox_theme(self, event): # wxGlade: BaseSettingsFrame.
+ print("Event handler 'on_radiobox_theme' not implemented!")
+ event.Skip()
+
+ def on_about(self, event): # wxGlade: BaseSettingsFrame.
+ print("Event handler 'on_about' not implemented!")
+ event.Skip()
+
+# end of class BaseSettingsFrame
+
+class IPConverterApp(wx.App):
+ def OnInit(self):
+ self.frame_main = BaseConverterFrame(None, wx.ID_ANY, "")
+ self.SetTopWindow(self.frame_main)
+ self.frame_main.Show()
+ return True
+
+# end of class IPConverterApp
+
+if __name__ == "__main__":
+ app = IPConverterApp(0)
+ app.MainLoop()
diff --git a/GUI/IPconvGUIbase.wxg b/GUI/IPconvGUIbase.wxg
new file mode 100644
index 0000000..3520d5b
--- /dev/null
+++ b/GUI/IPconvGUIbase.wxg
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+ Settings
+
+
+ wxVERTICAL
+
+
+ 0
+ wxEXPAND
+
+
+ wxVERTICAL
+
+
+ 2
+ wxALL|wxALIGN_CENTER_VERTICAL
+
+
+ on_checkbox_monitorclipboard
+
+ monitor clipboard every second for hex or dotted-quad values
+ 1
+
+
+
+
+
+ 2
+ wxALL|wxALIGN_CENTER_VERTICAL
+
+
+ on_checkbox_stayontop
+
+ always stay on top of other windows
+
+ 1
+
+
+
+
+ 2
+ wxALL|wxEXPAND|wxALIGN_CENTER
+
+
+ on_radiobox_theme
+
+
+ 0
+ 1
+
+ dark
+ light
+
+
+
+
+
+
+ 2
+ wxALL|wxALIGN_CENTER
+
+ \n# Copyright (C) 2014, 2018 Brandon M. Pace\n#\n# This file is part of Quick IP Converter\n#\n# Quick IP Converter is free software: you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or (at your option) any later version.\n#\n# Quick IP Converter is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with Quick IP Converter.\n# If not, see <https://www.gnu.org/licenses/>.\n
+
+ on_about
+
+
+
+ 1
+
+
+
+
+
+
+
+
diff --git a/GUI/__init__.py b/GUI/__init__.py
new file mode 100644
index 0000000..07b437d
--- /dev/null
+++ b/GUI/__init__.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+from .IPconvGUI import IPConverterFrame, SettingsFrame
diff --git a/GUI/raw_resources/ExitIcon.png b/GUI/raw_resources/ExitIcon.png
new file mode 100644
index 0000000..10d117c
Binary files /dev/null and b/GUI/raw_resources/ExitIcon.png differ
diff --git a/GUI/raw_resources/IPconv.ico b/GUI/raw_resources/IPconv.ico
new file mode 100644
index 0000000..7eecadc
Binary files /dev/null and b/GUI/raw_resources/IPconv.ico differ
diff --git a/GUI/raw_resources/IPconv.png b/GUI/raw_resources/IPconv.png
new file mode 100644
index 0000000..6c804ff
Binary files /dev/null and b/GUI/raw_resources/IPconv.png differ
diff --git a/GUI/raw_resources/IPconv_attribution.txt b/GUI/raw_resources/IPconv_attribution.txt
new file mode 100644
index 0000000..1f5a425
--- /dev/null
+++ b/GUI/raw_resources/IPconv_attribution.txt
@@ -0,0 +1,2 @@
+Main icon author: Arnaud Nelissen
+Main icon license: Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0 - https://creativecommons.org/licenses/by-nc/3.0/)
diff --git a/GUI/raw_resources/SettingsIcon.png b/GUI/raw_resources/SettingsIcon.png
new file mode 100644
index 0000000..335f373
Binary files /dev/null and b/GUI/raw_resources/SettingsIcon.png differ
diff --git a/GUI/resources.py b/GUI/resources.py
new file mode 100644
index 0000000..d6d5ce2
--- /dev/null
+++ b/GUI/resources.py
@@ -0,0 +1,631 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+# Main IPconv icon stored below as IPconvIcon and IPconvPNG
+# Main IPconv icon author: Arnaud Nelissen
+# Main IPconv icon license: Creative Commons Attribution-NonCommercial 3.0 Unported
+# (CC BY-NC 3.0 - https://creativecommons.org/licenses/by-nc/3.0/)
+
+
+# Data converted with img2py
+from wx.lib.embeddedimage import PyEmbeddedImage
+
+
+ExitIcon = PyEmbeddedImage(
+ b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACv0lEQVR4Xl1TbUjTaxQ//zmp'
+ b'NivJyPeM2v4r1CxFifZBg8JiKCK9YCBkl/qgdEGNIZVBXDJ8wVX0QZdLSytT1kBEMD+ocLlg'
+ b'1+VVMcNcvm3zPZWcMtZ2Os+D27oednbO+b1tYxt4q0d5SD2ijMb5hw+eutd/hMC2QrdbuqSr'
+ b'fMQ0epVK7cV95mEibBWPcFh5ECf+yP2ALpfsd81saUkt01hJM0QaFuIzDxBg11XhmsnIJ7vH'
+ b'rl7u9oZMa4vq/9um6feGmMUY/sqrb9/gajPr1/xmuKXoz+aZsr90ZhJbK8o4t7Klo3fCQwRr'
+ b'dWW5teaZNir3GsgiowBAoEZw2Kxgb2xgK0QQFxQdTTs/iZsB26sG2HdB0ya4NzflltI7zxfa'
+ b'TDkxFy/BrvAIHsJqY9bOw+ThkWREQgWOTbe2gDwxqS+2pi6LK9GD0tHCW00LHe1XDmk0IDsQ'
+ b'ykMEbvPXxsI8THZ0QNCJxH8SDPVZgbv3LPKArRDJ6N2SOpuxJU+Rmgo7Q/ZTgL82l5dhvLcH'
+ b'glNO9Z6srcuWyuXfGS71CgSJ4Bl78njuJ9k8DgdgYCB4/H7C1sFFnDQsfClAJlvx+bzLSEV5'
+ b'9bhBX3j42FEIDpIDIGOJRvSFzC8ugX1yEsI0GabkKl2OECBxAno8khFddblRdQQHz6XhVEY6'
+ b'TmWe53PwbBrH+M07nd9GUYF9t4uNbqdTBl/0tfebVQocUKfgxJnT1Gr8RpPd70QFtRI/0U4Y'
+ b'7wkvp1Lix9J7TdBEizkxHi0px9GSnMDbnBSHDO+6ntf7d3FRK+0M45pxapp0x3MNsJ/jS1r6'
+ b'Y5X4NY5PZPf7zIzPzrW1UEQM6Lp5o7NBJfo0Zq4R/f8HthhEEf9VxqCBCFN29hAz+74Bl2tH'
+ b'V35++wvVlkb0mf3FAD0RnQUFxvW5ucOwrVwOx95urbaRdP8z/wJ18ad1OOLfGAAAAABJRU5E'
+ b'rkJggg==')
+
+
+IPconvIcon = PyEmbeddedImage(
+ b'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAIABJ'
+ b'REFUeJztvXmUZMdZJ/qLyK326qre1JtaiyVLLVuS3bLUtizJWJbkZXi2OSNsvMm7/RjmGQMG'
+ b'sxrw4fjMzGPO4R3mYYwNBg8wIAYDjxmwjQ0yGITwbuMNeZEsuSX1WktWrvF974+ILyLyVlZ1'
+ b'dde9mXmr7k+KzsybWXlv3ohvXwIoUKBAgQIFChQoUKBAgQIFCmwHqGFfQIEC58LdQOngQVTX'
+ b'er91+ZHS/3vfV5YHeU1bBQUDKDDy+Nm7v/+H3vHyF/8WGNzv/fJlV5dLs/N/1fr0X/6P9/zJ'
+ b'Rz7yn+79m4VBX2NeoYd9AQUKnBMlVFWpMqnKlal+A2Sq7Ue+81J98Ol/9PM/956HG9/48h+e'
+ b'+eP//NJ3POvJ08O+9FFHwQAKjD7MBj7DDO60YM6cnGkf/+7LS4du+NNffO9//07ja1/83YU/'
+ b'+C8v+uHbjkxlfp05RMEACuQCvM6w/yg7FAClwO0WumdOzrcff/Q16tKjf/lffv33Hlz56ufe'
+ b'f/b33/O8H7/j2snh/IrRQ8EACow8NqYAECwnUNaxpZS8AW410T35xN7OY8ffoC658aO/9Ku/'
+ b'/bWVL93/G6d/91due/uxY+OZXXgOUB72BQwbd999d+nIkSf6OkOPH3+yOnPmDN17770bWYMF'
+ b'sgT39f/Zt8BC+14DECbAAEAMKLKfaTdV91TzIIC3lq945lvf/b47v/Nu0/yLT/zph/7HZz75'
+ b'kc/90n0PNbP/MaODbR0FOHLkSPVLv/cb/4BSub9KWKtV9K59v672HPh/BnxpBSK88+7vv+ed'
+ b'r/iBD671fungZWSWlrXSCtAaSq1e1gwGyD4TZsLsGAcAPT4BPTn9b7x86s9/5UN/8se1k5/6'
+ b'wi/d+5V2Bj9npLCtNYD/9h9e/0JVqT0DpVLvG7JAJmcZ9eUbh3BpBRJYW/67N5kAVu6lEkOg'
+ b'F5qjzwfpx8yglWXQyvIVgPqJn37Vy3+8PPXmr/3Ejxz/8C996C/ufWhh4UtbVQvc1j6AW48+'
+ b'7S3Q7haI+ihgQJcqAHWfc/fdd5f6/X2BUQGDmcEMMNlHYu4Z/n22DIIBsFKWZ2gFKO1MBgY1'
+ b'VlT7xPGru139Mz//hld+7oO/8JOfXbz/o+/66be+/inv2mI0s6V+zPngXa973WUoV24PRyIO'
+ b'wADGJ4BuR4Gw74o9c9cN4xoLRFgvDCCC3VP4uvqChVK9TF/bY0op/xxgUH0J7cePX2vq7V98'
+ b'52te9cWf/NKn/2X5U3/9zj/5sXuuSvcHDgfblgH8whte9lpVqVV6j8riYfDYBKPTBgD9K+/8'
+ b'qduGcIkFIsS0vWoAzgRwhE8MEG1gBB+AO4uNJDpGoJxD0X6nAdUXVfuxR57eaXbec8fdr/3y'
+ b'8hcfuH/p7/7sx/78x9946UBvRorYlgzg6NGjFS5VXtV7NFpRumzDR0RgJlCz8bzhXGmBgHXE'
+ b'PzOY2M9Zz1yez4hhwwhQ4MAIlFMZjIFZPFtqHX/0pk5X/+pzX/6aXxvgjUgV25IBvPhpR+5C'
+ b'bexSdsnlDFkD1lakiSlWzYZ4lACiZx89enRimNdc4Bxg9rkAzOeyF85jiJ/AM4HwqJjBnQ66'
+ b'iwu5jaZtSwbwM/e84i1Ki19PJEBQCXWlomKJoJSaef4znnZsOFdbwGJtImUheo7nMoXBdqge'
+ b'hgKfcGgvK2lG5AvbjgG8682vvpgrY3dwj8rn1EdmYHwS3GlxSCqxn/jln/yJwgwYJs4prMnO'
+ b'n2PiF2IBrGsdRPQfLskepA3lKo4mtl0ewC1XX/VaVa3VAHEeuX/EhhwbZ91aUau4eqfzfQO+'
+ b'1AIejrjXUrQZdv5AYAUoYhvay+xy2DEFpzVSdqfKGttKAzhy5Ej1tmccfZVn5WL8k3vU2kaI'
+ b'hfh71hDf8NP/8T/uHugFF7AQAltHEpPIY3mfOLvhnUbw580rtpUG8LIbr/s+VMauYEkD86lg'
+ b'zt6b2sFanH/yng0HQwFl1W48G8CHB3/lBcRkC7Z39EKkMdvPkdKQrMCMriY4HePwYw6xrTSA'
+ b'n37Ny9/i037FuBNnDzF0pabWnExmvPsn3nbX4K62QA8kPyP6r9fpF4cCM5T+7jzcc5782gDb'
+ b'hgG8+7UvO8TV8TvDEbdw4Oy48Umg3RD9DkHPjF53u7cVacHDgfX0RzSPhDUgjEAeYbIb8Xk2'
+ b'mnk4otg2JsCxa65+larWJoPuGNtyDDUxSWg2NEuIp+dj3ilw1eW7d18K4MGh/IjtDG/ch3Rt'
+ b'hnvpE4A0oBhKA5yxCcAUzqsKDWC0cfTo0cqtTz/6GiCSGs5eZGKwLiHkfEbSRRYZKy9qnnvj'
+ b'Dc8Z6MUXsOiZlGj4xD+RyGSZAWc4iAA2LlNUEo/yiW3BAN7zgy+8FbWxq2Kpzz6EQ8DUDKvm'
+ b'ihLnjnj9oBJmADNuv+lpLxjaD9nOEOJblQBg07X9IKeWZ+kDEMHhzkk5jgNsCwZwy7XXvlWV'
+ b'I2vHJ4zYxaIqFe9F7vlMnAHi1AYmfubb3/72bd1GatAgmEgB4DAgU+YYuWHPBIizGxw5ju15'
+ b'82sCbHkfwM+88qX7MDb2AgZDsbJSXWiaCGp8Etp0lbcLlHKEH9mQonoCUOB94/X6NQA+PfAf'
+ b's11hEHIBeno2OI4tWp0iKLJ+AN/8IwsQvPQPQiKf2PIawM/94EtegerYJKKccTuMtd0mpxjt'
+ b'Jnq9/vIYDRWO3Xrs6bevOlGBTGFTfUTVFw0gUsWj/H1OqxZgjeEJX86bYw1gqzMAxdXx1ztn'
+ b'vmMAYisyoDSU0sErKNpBkvjjYwzccdMNzx/Oz9m+8M62yNknxMi+dDua39hUSH2ENSTnzSu2'
+ b'NAN418tefBtq41f3JI0A1pFjCJieZbVSVz2BZWC1n8kfs99BXTr6rre/YX7AP2f7wpjVSTje'
+ b'+y5mgJXGcTJQVkPCfz4TMMfFAFvaB/COH3zJm3Wl6pR3duE8t5jA0NUa0Kj7rFGv5fsXiJiA'
+ b'FIAwlMJ0ewVHAXxscL9me8OnAqsQkhUHoJgArocHGNy/KWh6V2Np3jiHoyk0gJHDT9/9/N0Y'
+ b'n/p+jqV7lDKqapNQ3W7P3zAQmj/Ex+AYSMQUfvltP1KkBQ8Q/VTxEJsPGp6EdzM1AaIIkpUK'
+ b'RTnwyOGWK578Sj0+MQVJ+SXX4Idt6EZNTbFqNZQXGwJmcLKvfDL5hAHVbhaOwIFCxH5Q0ViF'
+ b'Y0w2Gq+VBmvKVgNgdrKEfPgxr9iqGoB69tHrXidOu+D9dxOmNJQu9eaSA+hZZPFIfkoBxOrq'
+ b'n/3h1x8ezM/Z5iBAmnh67z+CNkfMICIwGfeY7ZBzwdjzgfOrAWxJBvCzL3n+zWp8+qk+ScSr'
+ b'/85WnN5hM//YfwLhSdIDyIEvyEeIoUC1W264sWgTNgAYxB5+DoGAWA2nEJpDhg5AqQD0Dkjp'
+ b'LpxTbEkT4B0vfdGbVLWqgp2oAacFgAil8QmFlWVf7AMlTqY11EbPGBA0Agaee8P1LwDwR9n/'
+ b'ogKhH0BUyGXfiGx/2CnUURZ3ViAGGRsJMJzfKMCW0wDedfed82py+qUh5x8+bgtjM//QbffG'
+ b'/eI4bh8FILzXGydUHXPrbbfdtiWZ6KhBcgAk/BbH+8EENiEZiMlkmgpMZIJJYPKtAWw5BvD0'
+ b'iw+/Qk1OTbMvE3OLwtiFoadnGK2GElXSE/Va6r83A5y5EHmDiczhY0++9OqB/8jtiL7eePQm'
+ b'5BCBJMybYSYgXPahzwQsfACjgbuB0rOvfeprAdUTsmE21mmjFHSp3CPVPQ9Y5elf7fm3mp7k'
+ b'EwBKKf3uH33brUP5sdsNJPMgRTicYAqSAhxCgdlVA0bOQKd95BVbSn29/IXPuUFPzR5lEBRK'
+ b'doEgqI56flfI/ANC5g8rG/pbT5PzmkKkETCAlaU7Afy3LH/XtoeB9ba7bbs8JB9AJL5yvJnt'
+ b'Xp+ZgUI4GUSgIhV4NPD2F93+ZlUbc/TpdoUV9d8QyhOTChSpa97GP5f07+MrcF/AxDcfO3as'
+ b'KA/OEAYGMkcifeNmHJw4jvgzWQwfUTLueWECDB3vfNGz5/TU7EuZXaPmKOzHZKDHJ6C6nUiX'
+ b'd8MTOW1gcO9zMBTzztuuvuLokH/+locvuomz/iQEFzkBAyPIMhQofQHYqv+FBjB83HDgwN2l'
+ b'6R1z3i4k9skabAxKs/OkWo2g/jO8Lb/hAfc38gVuIf7y//XDxa5BGSP4cxKOWCan6Yln3vho'
+ b'QGblwD3P7frKK7aKD0Dd8tRr3gilbCEIE9g5Am2CiEKpUlHcbto94IWalXPenC8D9w4o2MfW'
+ b'yvMA/GJ6P6dAD4zt3aDAYFesYd03wf4XR5xS2vkCMk4FtlEgsCFQjp2AW0ID+PHvu+np2LHr'
+ b'aDDnnXpoCGwYpfndjJUlxU7t98Lf5wmcz5BUVHbfRWCioz/25jfvGupN2OrwcX6n8osK7rMD'
+ b'k76BZL5AmkMyAqNchJxiS2gAb3vesTeXxic0M9udXF1ihthrlakphZW69xKvvf3XBiAhQcCb'
+ b'BorV2O3XP/XYfwX+Mr1fVSAGwWpymsjv+xfMAQIZY6eSbBZgxgqA0wDIqv85dgLmngEcvWxu'
+ b'tjy75weYCErBhmScnc6GoMcnoQy5WB888doiQOn/dx6Q8J93CtrvvP3oU56PggFkAxd2UwAI'
+ b'ym7+iUjTkxwBMta/69KBswJLGNDIJiEFAxga3nHTsR8o7di5y8b8pauXFIQQKvO7GI1lxUol'
+ b'mknw+RN/H/gU1XbruZv+sgJ9QSBw10p+pYIfliGOQWuPK2hoUpb4TXYqALsiIOkQrIpy4OHh'
+ b'mUeufhO0tru0spRpukdWKFerCDFka7cH6X2Bw1WehZZUDDCufOfrXnfZcO/GVoYjdknx9sQf'
+ b'PPGS7s3GgCi7wVIGbJwJUEQBhoMfe+bVT9O7L3qG9fy7pB8oH6qp7tkD1ZDMP5sellazKOsD'
+ b'IEgusWKUbn76kVvwO/hWCl9fIIFQfqtgoz3wWh7EKQey2rgegA/A1x+IIMgncs0A3vqsm95Q'
+ b'mpgssyNERdqp/wQiRmV6ltGoW4pXXm9MTf23XsXgC3jedU95AYDfTeGLC8QwABmC0qL/27kj'
+ b'n/NBYOoCKDlGcP6R3fNFyDmwWkFekVsT4Mju3VPlXRf9YG91mIvLdgl6bAIlUdN9Eo/L5Duv'
+ b'7J81hs8GjLzRhm5517velWumOpowiTCfs8K8BLaxeHIdgchE5bpZDPf9hoztCZBjBpDbxfqT'
+ b'NzzlByo79+yO00Ktc96ujtquPYz6koKyveMUO9ewDwFsEl4lFYbAAGP/yoNfvRbAZ1M4QwEH'
+ b'A1h1X7GdO6m/d6E48pWACuw98hk6Ad2aI+OcgVQkAg0cN1195RtZq17JQAQ2XTAUKrUxRITp'
+ b'6DXqEbDZIY5FICgGYNzytOufM6x7smVhpNdf5PiLavFta24nmbvuMevRtfF/irIQ84hcagBv'
+ b'veqqp1T2XvQsmxGmvHfequEGtd17WTVt049ki+90jUPJAwgRgtuvv+ZOAP81zbMUAJgNWJWg'
+ b'fZ5HqPnw6cAAmJyPIFMnYCgKcrZIdifLGLnUAN56w3VvKM9OlWLbn8iAul2QIdTm5sCdVvAc'
+ b'J4g0tRGr/z4t2DzrHa9//fRw79DWgoHc4t5UXKv1wRYEGQOmLoi6VgukDIcJj2QMTBEGHBze'
+ b'fvDgeHXfnh+yHX5KnvMDbPP+JyZR9o0+gND5Ez0e5HQg55DCIACMabQWbgTw8RRPtL1BNgqg'
+ b'tY4KuMT56wjf2PWg2W4jqjMUysw2ykTEIGOgc8wAcqcBXH7o0Esq+3bvDdKfnA1oQMagtmcf'
+ b'o76sZLs2iROHllLpj5721Mz4xTe9odg0JFWQI7rg3ZdmL2RCGzDyHvpsE4G8P8L5IijHJkDu'
+ b'NIBnXnX5m1RJw5f6uhJgGAJBoTY+Aa4v2f3hWUPBOgoBlXqraBbvn+SGM9nn9ZWCAaQMNgYo'
+ b'6WDjg0NPPnEIwuYJ2XTglCc7vhZyWoALBxbbgw8Ibz58+Krawb23iifeSwUn/au79rBqNxTH'
+ b'qnkIAcDvMZ/SCJEA7nlOiq7/0Ve+ct9Qb9ZWgokcbxyVAMvOQK4m3zrljDMLMswD4C5CzokB'
+ b'57gWIFcawGuuu+aNldmpEjFBs4KisBCIGJO79ig0V2zWn4IjfO2LhLKAZzYuQQVMUIxqudN4'
+ b'FoD/mc1Ztx+4x9zS/lhsBrICiBQUKPNUYElDZmaYHFcD5kYDOAyM1Q5e9HJm2x3Gcn3jH0tj'
+ b'4yhrxb5fu2QAMvmFk8VAnF0YNaf4ude/8gXDvmdbBYReiQ7Xiy+W9OSHiZ5nPMTvkOONQXKj'
+ b'AfzI9df/u/G9Ow8wsRXn2ub/W8cPY3r/QcbyovJtwRAl6WQJOQmLNuBGq3nLbbfdVr7vvvu6'
+ b'5/qKAueAgbfxWSvrdGOr9fXk5IOhRNXLUAMAw3WaNj4ZKa/IDQO48YpL3qIqJfief2RJnI1V'
+ b'A8empsHLi445aIDcYriQph/nC58R6FRU6xS84sY9s5ffB3w925NvD0j/fyJGKfLtSE2+5ONb'
+ b'h7zKnAGIFsBk/U95RS5MgFfv33/l2IG9t5JT/6Tgg4yB6XZR27sfulFXiMpzV20jlfGAZKW5'
+ b'8ysmdfP119867Hu3FWDgTD12889sAy4+PViKgSQ+H8XpMxjGfX9IQc5vFCAXGsAPXXXla8d2'
+ b'TFYBy/EVuZZfbgImd+9hajWVUgpQBOWKfrJ0/sVg/684qazSces1V74QwG9lfwVbH8QMTQzW'
+ b'ZE0CV4gl0p/IQKtIG8/QC2hTzm0SEJHJ9e7AI88AjgHjkwf3vEKaMGoF2PZ+1hGkxyZRK5fB'
+ b'LUnFszkCSnaRHgQHgDsXCytwC6LTuenYsYPj99//SGMwF7FFQUJ0BNau5ZdyGoDLByFiKEUg'
+ b'DShSmUYBLO+RMDTnOg9g5BnAXVdffdfE3vnDzGSTeqTogxnUJcwcOsy8vOiSf22DDuVCf7AH'
+ b'B4TYDJDcA+x75qHrnnr//Y88MKir2JqwTj7WGswaAEXJOKKOM0hZrUDD9O4hmDLiHAQmhslx'
+ b'FGDkfQBHLzn4Fl0pewngw3+dLpgZk7Oz8FtCwTrgIFmCAx3szx0nrfz8PffcMex7uBXgbX0J'
+ b'u0lSEIIJIKW53ieQ0ZDvN64JSREFyAgv27378pmDu59DRNBaEvqUXwgT+w9BN1aUtAQH2xRQ'
+ b'dn3jBgpR/6lXE+ClhTsB/MpgL2ZrwbgwIEO7tF9nAoozTpKBYHOEiJCpBiBMHq42Ic8+gJHW'
+ b'AF58+aX3jM1MjvmiCzIg6sIYA+oazFy0n7jVDIk/rirP5+UPcrj6dO8B9AlIfP07X/2SnUO+'
+ b'lTlHKADy+f/xo7QA82skwzRgr4WEtuCFDyADHAUq0/t3vVrcaraeR9J+CaXxSdQqZUUt6xsI'
+ b'4X7nKxiGWSb5ANKswjKEmRuedOQG4M8+MoQr2jIgZigiu+8DW0dvHIozZFOBrQOQs3X9sISi'
+ b'7Xl1wQDSx3MvvfSu6T3zl1guD8Dl90kN9tyhS5gXz1g9z7X7htv8I9ONIdeDTwiSEmQbErzt'
+ b'yZfdBaBgABcIA5cJqAC4zs9iAoi2x2RsLQAroAu/fVgmIFf05c5bbA2WAa49tO8t5WrZ2lla'
+ b'uVCQq75SGlM7dtjMP60AafqpXGOOwbn+V4NDeTBLkVCzUTgCNwMDEDG0jqoBgfCcpBDLZYZq'
+ b'9n1DskDcisybnDnFSPoA/t3c3MVzB3bdwWRcPQ/5kl9DhIm9+1DutOw+X1GxT9gjbrijt0gI'
+ b'YOIr3/biF18y1Juaaxjn7XcFOCxEGDL/jBu+SCfDwRzOZYpEoPTxwoMXvWZydrLmJ9kdlxDg'
+ b'7P6DTI0Va/i7jB9f/TsK3NinCEOYUvXmI1c869f+HN8Z9qXlEgYgY2s7bNmv8j4W6Qxk14mC'
+ b'1gZkFPQAGoIYCppAXjGKDKA0e9Gue3py7Y31tBPZzL/xWhXUaUBD2biPUVbtA7ItAtkInB/A'
+ b'hgGdrQrGLVccugvAHwz56nIJm17hCN1osKLota0GZWMZA5G2TICyTQQimV9D4G7BAFLDnXNz'
+ b'd8xdtPNJcbMHKIYhgLsG84cvZTp7WoEBKmm7J6Bi6x0elvMvAalck3wAZ6s+F9bkyq++ODS4'
+ b'3v+q5P0AADvBy/4/aRSTuU3OUSUiGJTjKR05H8D/cdmhN1cqJe/ZJWNTfqnTBRFhZm7exmFt'
+ b'TSZCc0Z5HP4QL3F8TcS0/20vvOPaYd/fvMI7+yDxeO6J+4d9Ahnk+rNkNUICqDNBhul03iRG'
+ b'SgN43vj4/l37d72AyXpyxZlGzDCGML3/YuhWw5oEWoG0hmKCIpH+GdeBbxQ+GSlOTGJ97MmX'
+ b'PfvX/jc+P+zLyyPIOVWJtGv6IdqVRANsrghre9+zzM+35yQ/z3neGmykGMDFu3a8dnpmakzS'
+ b'PW34X+q7CTsOHGBu1BWUBrOyXX9Ytvx2E84YfBpwEhKKlKIgZw7c/KRL7gLw68O9uPxBPO5K'
+ b'wTsBCWFdSDRAKSsstEsGyw7igAypyHnFyJgAR4HK0Yv3v9r2gA8OFiIDMl3o2himajWf8gnh'
+ b'+E4f4zgUZ1x65rBCgZyQTtInoNu55e133z0+7HudS8T+FPJp1pDNYH1EQMyATIuBJPogjGDY'
+ b'N+fCMTIawPjk5K27d++4yqvOhlyXV6v+X3TpFWwWTikwQ9n4mpP82jv/WCX4/jDDAmIb+gVK'
+ b'gMFs98yJGwHcN5yLyi98+I8IpBVgYp9PiBiR07h01uXAYt65dOS8YmQ0gDsO7X3zWLViVX7Y'
+ b'Cisiu++aIYMdO+cUG2NvOuzWzGyCGkiO6xsjSSH2M72togY5yF8fu1oGBuMn7n5RsWnIecKm'
+ b'ArN39skakWOibfmcAMSxgSz+c9eTa/efxUhoAEcnJvbt27fzhcTGhvOMTe1hInTJYPrAYeiV'
+ b'OrMhpTSDUXL2IGzPr6Skd68Z8VtD0ARcYpKtGLM2IxYXngvgFwZ/MTmGMTBglETtJuNj8YEx'
+ b'kG0UrUtgIpiMy4HJaanEBFP4ADaHyybHXzk3Oz3l1TgKtj91CbsOXUxmZUWRu/Hk9gbo2SEm'
+ b'VgX7vabV6mL2w7jUUSex7G97xn94ye1FefB5wu8JIBmW9qB7LeXA1jCUiEFWI/h1hLkXxUCb'
+ b'Qfm6A7vuUS6sAxiX6gl0yUBVa5iq1ZRZWbaef9a2KEyRbfoQ9/zrowlw0h88yAiBqKNG1FQD'
+ b'EKp0ZuUWAH82uAvZAohCqt4JxwzDov4DAKHk9gXMUiaL7U8uGSm/8n8EGMD1Y2M379s19xSb'
+ b'8adgWLkkIEbXGOw/8mTqnD6pASFtgt3mG713Xq16MnRTQJyA3jnlHFc/9sLb7/yN+/6pYAAb'
+ b'hO2+a8OAhrQ1/yD2f5D+TLB9AcFQWZcD+yiPTQrKK4bOAG7Zs/OtE9WSy5YDlLF3teuce3M7'
+ b'dylePGvJViuwa/kNUK8091SOxDH7RngqXCNrRsAhUcQnBTnG1mzeejdQutf6twpsAHIfS7H6'
+ b'zeEesyQGlVxRUIZimRGckHl3BQ6VAVw7Obnn4r1zL5LJVcRgGBhiGGMwvf8Qyo0miI0jduVC'
+ b'gLI5ZPRlsmt0krCd1PdvR8eyBUfpo+KDEB8HH5m945aL8bG//3bGF7F14NRu0feT/hZDBJQA'
+ b'ohI02CuJmVyKy0EhVxFIRVPQC8Ouiv6hXTPj0+TSeY0jli4ZmC5hz+FL2TSW7IYfrimIp3Sd'
+ b'VPXXgeI+voB+KkNaCFmJ8aKFqyBTxOqZFx9+zvtRMICNgphQYm1NAS12OPnQ3yofQcYMwNcA'
+ b'cL6dAENlANfvmn8D2Nb5awZY2dBOlwiqWsXM5ATozAoYQIkJrDSUSwCyXp9+0r7fbFimwT2l'
+ b'AgkHYprgcB1xVJpcbgOIceO+nS8A8DvZXMDWgiG7RhTsxqCKlI/o2BCgsRqkaASKM9bvRJOz'
+ b'uQeU41TAoTGAy8rlZx/cNf0UhgFzyeZ7Ozu5awgHrrmOu6dOKiiGBsBsu36SgZUAwGoC7jsP'
+ b'atUbSv7NQgng8MQmjMCFjgDpb2hj151j99x229jv3ndfM+Ur2JLwSTjO22dDqwiSH44YhRFk'
+ b'bQLIQL7ru4fGAI7unH3TZKWkLGe34Tqp+usaxu7de8DLZ6GVDgTvnHi++GLDjFf18AGVPJ4m'
+ b'Egwg+AA45K1blfVQqV2/GsDnUr6CLQhb10GKAC75+9rTops4dI1Gxvn5TuqL9OeiJdj54SAw'
+ b'f+nO6ZfY0I6GcuExYkbHEGb2H0St27FJNAq2tstPqD4Pwhf0Un4Q/Krn+KbB8dNgG0qDEJ+7'
+ b'7jrZ3HTpgdt/+58+XTCADcBHAIgAUr0SmBjGOpBgFAGEzGsBhPiNkXPnE0NhADvGa6/YNzU+'
+ b'Y4yr91dWRTZE6BiD/Zc/ibv1Bev8s+ldAMi1/F7H47rePPRdD1lpAFFgyOmn5NV/29eQmXHD'
+ b'7p0vAPB/p3wFWw4G8M4/63mXdUF+oxAxEsAKrHpmIHXYFmWOsefcCBgKA7h6bup1TAQDBtiG'
+ b'9IgJHUNQ1Rrmp6fAZ0+BoaCZe83485nXOOTf9++cLpCqBsDRU68COC2AQhcZW0v+9DfceWz+'
+ b'Ax+9/3RKV7Bl4eP8kk8R5VfAaY/K+1qs1pjZtXC4DikLzisGzgDmgWOXz05dT90ulHYNHmGl'
+ b'f7trcOlTrmc6c1oxM7RyHn8Z66h15zsFmfgB+kl/wDuupICEjdiuvOPonn3XfwD4REpXsDVh'
+ b'jCV6KEtsWrIrpWpUmIE1FUuU7b4gLqkThsXxmN25ssbAGcAlM5NvnClrbVxMHC600zGELjEO'
+ b'HjygaHnB0nrJbQTPbrefNV273OfZRpDiKvF+ydhBKYUrySIlSWFl3LB3910oGMA54dt9uXvK'
+ b'Ud8/Nq4TEMv7WdcCkM8BIOZiX4DzwI4nT0/8+27X2LJN59kzxGgZg7kDhzDBhg2RUm47YLsf'
+ b'vGTynWNauff9cy8ChkssTwe8hvRHSFDxlYlktYHO4sKdAH4qpSvYknDtr1GzAAAgAElEQVSp'
+ b'E1Au+06zWw0cpeO6PCsQ236SGV4PAxKY8FmBecVAGcBUWb/s8ERltktWpTOwdNJ1GsAlV13J'
+ b'nYWzSsJ+TBrWBeRy+Tfi2eUL0AbS8BivQ/ziA7CbV7hHMi6fnK569dGjF3/oM595ePMXsXXB'
+ b'rgiMndrtm4BKbgAkJVjZRPGMNwYhigVSfm2AQTIAdWRq/PUgshV/juiIGW1DUJUq9s3NgU+f'
+ b'AKB8VRfIkb9CwgRYj2h51Zwkp6ineDCN+VuTATgzgJNmgItZGx47evHumz70GRQMYD0QwCrO'
+ b'/w9ZeESuC7AisNsXIOv9OiUL0RDnuiHIIBnA068er97Y7drCHnaatyHLAK582lGms6cUkYGG'
+ b'bPghNf/BUbcm2feUBjiq3ki+UCrxYo4UD3b/u8fYBGBpY8Wh4SkRrpufej6Ae1O4kC2LuLkL'
+ b'PCOI8iqcX4C06xmZdR6ASzayLSzyawMMjAHsr5beNKMVOoZASlp6W/W/S4zLLj6kOstnoZWC'
+ b'YYLSClopVwQY+ez7tv8CRElwT91n17bvU+XZPU6n1YTvpX/MBDjsLccrze9DsWvQOnBRAFYh'
+ b'6i4OQIR7S2y3j1eaQ5v4DGAjuqH4KL+1gINjADNHxqv/vuPSfqW7BxOjy4ydBw5iCoYNsdvV'
+ b'2TZ0YCnljaV/rLuvsgjsAd3zufWxIb/COljVE16YQY/kFyYAMNnCFdvU1O58xEwXv+raq478'
+ b'9y9+7cubupgtDGGeEuiX1B9JtTbxPXbRpQwvxoUCremR42rgwTCAGeDYpVMTzVLUnAcAoAEN'
+ b'xqVXXtHV1epZvWPHLEy3Qp0uqNsNGV4uzbMHPSZBLzMgaQ/uNAgx8/vTeooMoA/xh0UZ+QCI'
+ b'vfrv+gOU3nTzM55dMID+MLBSXwNhgxAv9YUZQHIBQZzttiDWNRXCjaZwAq6PReCjX1LjV8xQ'
+ b'fddi0+zbXdI37Ruv7D3VaD2rYXjPV//XRw8prXbumByv75ubP7tn9xx2TExOHdg919gzt6NT'
+ b'0XoMTNMg0qbTAXe7Xq1GgtuLIqBcAxGtEPYN6JNMtFlTsUcBELcDh+M9DkCxYdn4vQJkb/vl'
+ b'k4/fCeC9m7uaLQqpoFQM0jbeT8x+xyCSRCG3SxQDrnFMNhBxJMlIebbcBuYDuP+RRxoAvuvG'
+ b'A/F7t+2b2kUt7K4pdXl36ewN3zx9Yv/Jlc6NTeruaHRof61aVXt3TJ/Zv2tXZ+eOqbH5menq'
+ b'4T2767OT40qDJxXTOHUNqNMBma6LHFi+TEp8CTaEFKeI+WMXimTCSaQBxO2qfHIKW21GCF9q'
+ b'2ZkY1KFbDx8+PPbQQw8V5cH94JXAuCwYljnYYBEUbI2+0pzpTtEs0QfnDCyiAJvEfceXTwI4'
+ b'CSx/FcBfRm/p23bPXLbc7OzcU6anPvG9R6/59ne6l5xuta7vGMx0mOfmpicbe3fOP37xnnnM'
+ b'T09M7pyZ7R7YOd+ZrNXGqNmY7taXQVpDSwN0m1awOqp4gVjLBFjt/HOvoyahRAzT9TnlO2+q'
+ b'8dGHgE9t/qq2FgxgHcMMECm/RTjZXZcjM0ukclqx3f4gfy5sToCMAEaCAawDuu/E4oMAHvzM'
+ b'UuOf4zee/yTUnqr2f2P/1NjFJaUn0FgAGguoP3EcX/83xsTMLK697ft48cFFpcFgKBdShE0o'
+ b'AVJZI6sYgEsTW9W0MmEGsC8Mkh2EGK9+2lOf+8ffeLhgAKsQVftBzCr4KIC3x2VeYZ9ngfD9'
+ b'URQgvxbAyDOANfHXD6K1/0Cre3B6HEorlJQKdhmk/NbZaAZgtgxAKZVqiDhmAKIaAi5LLbER'
+ b'ScwMZEureGuz+pmztwN4d3pXt3UQ1P7gaPF8QEJycDtGsoLOqN2bc+nYNeaTvPLLAXLLAACg'
+ b'y7bVZ0kpaG2lO5GyeZpKeUKzKr92xM+pMYAejz/g1ftEB2B4iRE9J6cFmEgbANNNL77uuh1/'
+ b'/oUvnE3nCrcOyFX4+bJf0Z7kPsN2iGOGTzJLGxw9sksCMsh3b/dcM4A4L0ACP0pxpAm4xBto'
+ b'O1vabSqSYq1o8Pazl+jsNgQln5ZqVvsEXGGLtJSyRS0Y4zMnbwbwv1K7wK2ChN0tRGgDQew/'
+ b'4FOBMwgEegZAUgLszpvjeuBcM4Bgx0s2gK0HlzIC233HEr7SCppcGDDFntFetUe8A7HtYFyd'
+ b'nmkppWrdto1OdLsdULttd7ph6QsQGoQwAy+76pI7/+LhRwsGEEGqATWHR4kAk6jjLMzfquVp'
+ b'WwCroz0hHbjQAIYEsfmcjHeSQfZqiT3uYUHYTSNSZABe8sATP7k8/0/8y+d/9dMnTv+fl8zN'
+ b'dnZOT03umZvp7J2Zbs1Oj1UUeLbbapeMMei02+i2WgARuu32c5BeidIWgjgBHfF51UveC7F5'
+ b'RjoRnt6zR89ZIjfhnHlFrhmAh1P9VNQx2Nrk1sGmWUHBNhUhqFTzNiwDIKdxSIWYVemfaDbu'
+ b'f/jE6cXvPnFqL7R6Jis13zLm0A6tazMT40t7Z2daB+ZmSjMT49N7ZqdXds/PmrFy7Ul3X9u6'
+ b'5N4vfvHb6V1l/uH6AXmG63Qu74gTyc9scz8AuwZS7/cqzkZng3DO+XTuGUDgvtF0u5UgdeOk'
+ b'4DrFIv0dgTgyA6Itq4gZS4TjDxL9fwDctkfAPHCAdWn+TLONh5ongcdPwikoJV3m8qGdu3Bi'
+ b'bOpUehe4teCDAOEImI1LAIp5e3qEmfwmYTquyzty3BQ43wxA1D6Cswuj194RyAxNDNK+DCld'
+ b'95B37IkfgLxE6ofTwKOnO51H+77ZAR555LE0r27LgOD2jtS9TFZyAMQbS64voPjlUtcAEDkh'
+ b'RRvpsz3wYWBsbP/+w6UylezfVFQVADHr2thYuWJMeaJWW/j41772jZQu8YKQewYQP1eIvfLw'
+ b'MXYo2yXG1oojdQ8RRUkhNkddWFGBNGAAm0JdYr/7D/n2aqEgSDFQgp0HFZkBwOYYASdehJwD'
+ b'K/27AJ5/87Hvn5udv+aSyy+9dnxy6pra1PR8qVw+oJqNdqlRr1e6TaoyTTZPnkD7zJmVbqtR'
+ b'qir+8Me/htdv4tI2jXwzgMgbjGj3oMAEyPWT1zBOE9BKIe2d4/wmlS4UqECOCRRICy6DOzj7'
+ b'ELz/QSKnL/mB1QxANA+bUcqA0uXnPfvZP1NuN59UUVzRZ75XaT94cpmbjRPtVndqaWVlvr7S'
+ b'QL3RRGNlBe1OexzEaJQrQ4/25JoB+A0awI6oQxiI2UkNIlcoYvsL2EhgusTJbuWJd1q61BZI'
+ b'CcbY6r44ldp54uFyKrzOxQwoter+p6kB+EcFKK1QHhu/XX32H0uLK01dX2mg1Wqj1WpOdNtt'
+ b'3zaMydg24qGFWPORTuufk+caNHLNAEJQyAb/FJQLAZJPApLNIxQxoBU0q0w2jrSqKflOsQCj'
+ b'k/5ptiV8PwCn+pPbc1lyACRNmBgwygqCflN8IdPej40zGFJvpKBQX1qsfHNpwYV/ZT2GLyjB'
+ b'Ribk75xx8q8PnGo8cgGXlCpyzQAivm9fJVi1LxIhhiQDZnUZYgLAaSSFBpAuiBldBjTZgKDd'
+ b'Rt52lLJl1ZIKbDWCfqkem5mRZBgQbMOSJQDGMDRstaKWmhT3GUlLVu7vDCx7mh6rfQxY3sQV'
+ b'pYNcM4A4FENwLf4lOyxStxgGJbYxIvlcFhcj1X2mp2V0gc2CAHSJoEiBNbs+DpYBmCgaoN0W'
+ b'8lBr6QAXjmToEQo9RUfSiMQPBthdJwOWERCc7gL805mlj6Z6gReIXDMAAH5mYtNMZoGMy7FX'
+ b'gHGbi9p+gWmnidlJlgQgiQgUSAe7qqVJ40JtxNqVciiwVigrbbdbg9O8oG06OHoyQzaNVRoA'
+ b'ZJkpx3CSxO+0EaWcIHIOQ9u25OxceezzQCulq7tw5JoBiCc43FgfnXVE6OLySqGk0l8U/joc'
+ b'AxAtQMKBBTaO5z/pSTNtbS550sFDOy/fv+dmjE3tVrM7jlGlekBXazvRaZFu1hdrptspd7oV'
+ b'NOtVU19aocZK6cS3vjMFhlbabyETIkFIO++j/zEVv+eZhPJ2qTULbdhQQ336HxYWzqR5WReK'
+ b'XDMA6SBsGYGtAw+tI6TAhl2ZMEPZFZI6GOy2ipLzMgr6X42jQGX+4osPYmpq9o4rDz+DJiYO'
+ b'6p17nq4qtSvU5OQupdRO3Wqs1IgaZe4o1Vqe4MfPNjsri8vN5Ua1UV+eXmx1SvX6ChorK2BD'
+ b'Ey7cYxm823CGEl7eNKacE096tIAoDCnEH1csimOYmKEIOM346xQuKRXkmwE4z6/fn813i3Uc'
+ b'lwiGDEoogbVrKwWkv3VsfE62HX8ZQKezPeMAt+3bt6s6XZmmiflr77x492V6fs/VVK1dr8Yn'
+ b'dqJWO1zqdqlGtFLldleb7hhOn2Hz6HKjU6+fqi+tTC+2mjvr9Qbqy8sw3c6Y9FiwjjcL29zJ'
+ b'FoErBdf30THeRJg3DS0gyc/Fx9Oj+nsdNLyWxiE+a1EB3202R2Yz2FwzgKTNBSQ4ryvPtY3b'
+ b'lcsIVOmHAzgKRbn2XttVAfj5G458eLxavouhxsGMzoknwCeeADOjNjWFi/btXTxx/Lg6XW/O'
+ b'rjSaqC/X0baVkFOe0B2kg5t/9Aqc8jxcwfl1EBF5n5ufqimwnhkQv8+uO7HXAgACvnt4/+Gv'
+ b'fPXBB9O6mk0h1wwAgHfA9R5yxEiRLU7WSMwiCuCTkTjsUrtdWcCjjz3x+Ut3TLzEWV5hXwYC'
+ b'Gotn8a3vPTL9nZMLCgBKTnK7Zk720b2wTd2jDs7un1XH4IhbqXMWeaaWDASJMnBM64jqFf2w'
+ b'OQwhN6Ci9T/+9YMPDt/755BrBiAd2YPaHxxyhEgD0AwpCZS+gGkitKUSE2D7RgE+dWLpdw/P'
+ b'jL8TUGNQVklXkn3JhKrWqqQVDLmojAwEhgE4QvfPe9E/xp/t/U42aAivV59XRe8olnC0vcYF'
+ b'jI79D+ScAYheJdzW54q77D9iA2IDRdqmAWu7J2FWHrqQpkp57hK1KXy91frO6Vb3E/O1ygtj'
+ b'+9z6ZxTahrFzosZP1BvWHQOrCaiEdAfW1tRWS+PBgXue9/oB/HHv/A+JQOI7fKTdvG8gF7pB'
+ b'5JoBeEnPEvbzdpbXDEhSxLSCdp7CLEwAcQT6arVtygAA4Etnl9976965F5Jzicv27jYZhjFZ'
+ b'qzLXm4oYIMXWL9PPebfBe7jRW51NTkDEBKIIgaQm+63L7K341y8utEZqG/hcMwBAiJ59TziK'
+ b'YjK+Oy/INm0geJU01WtAPOHkTZLONq0G+OappY89c/fMd8pKX6I0g1iHHd6tWqAnKyVeaXeV'
+ b'YQXNtkrzfDEsHst9njPgw/6MoAXEtSFj1dInMWItBHPNAEIpqPBht320u/HGVWKRBjRsR2Bb'
+ b'DJxRJiDY+x22sQKAh4DmQ0vN3790ZvxnmRTKOr7njLZhzI6PYam1DJBN7S3BpvJuhA8M+972'
+ b'MABOOAPjKJTzCRknHD5ztjES6b8x9Lk/Mtro8bpy7JhhXy1mqTPaz41SHpIBaEICEDGwTRUA'
+ b'AMBnl5Y/SMxNZnGAAfAOWMZYpaysW9bVa/gkKjGpekds4iXf28hQF/h35/ouyQD0xxBZMhx1'
+ b'pwLaU+XaP6R4i1NBrjUAW1nFoSlIVHDBYFueaSIfgCSJp14owl7dkyhAfveKSQdfWWw9+Jxd'
+ b'3fvmKpW72GXAKd+MRaHRNZibqPHJektpEEhrAOlt2tIPaX71ajMg0gKcryNuVqLB//wvS8sj'
+ b'1+sx1xpAbGNZLtsrRWTzSNlI0niJTemO6HulUQXneLuotPCvC/X3EiQj00p5z4OZMVmruEo/'
+ b'uLmBq6XAxgf1GX0+l9QiNjvQ53l8zAsDZjAUpifH/naIU7Emcq0B2E04EYifZX9Al5dPlkDt'
+ b'hiHOVsuoIYjPP5AS1fRPkTt889TyR54xP/3dstKH4CMC8NVzUEqNlzU3OkZ1XFu3UgbFGskY'
+ b'fppgfwIVdABnzsQRgH88sTAy6b8xcs0AgsSPbe9wzHZosbsCgWyyCUs6cIqQrkMGYgLYpbCN'
+ b'XQAAgEeAxiP1zh9cPFn7KQaLJeYCAQptQ5gdq2K5vWIzAUkB2qbPqvM01db7JJ/XN50fOHoi'
+ b'z30/CvhGMWcbze7Q23/1Q64ZQO+Nht8aykcBWMKCTvorG3KiDOQBIxQnUaEBeDywtPiBAxO7'
+ b'flwzykZZaxgIZbvjlTK0snOlldvExTsCNk66630yQ7dCH4S0X3JaYQn45ENAc6CXsUHk2gcQ'
+ b'9//36cBRg0jJzZeuvaIdpA3fo5457AtQqAAAgG8ttf9toWvus6nZgShEMW8Qqx21KgvDNhz2'
+ b'3Ivt6NUDqweCMBjU4OjcluhtL4LYl7Fnx9TIhf8EudYAhPjJ2fVC+ELohnwHVsC47aUVVmWc'
+ b'pXEhdhFEizPdM+Qa31hovO+G+cnbDQCwQgmAJP4yMyZrVZxotGD9ADaDRswE+8l+kDkcrHzv'
+ b'h3g1ifbXhfcFmfueOP33w7q2cyH3DEAkCjkHk08BduyXmFyjCPsHrmdE6hfSwwAgPolCBQCA'
+ b'T55Z/Iunzk0cr0DtA9k92uJkLNZQtVKJm8YoYoJh7ZgER//2I/Vzu/eydAAK4u9n0QaCEHj0'
+ b'y/XOlzO+hAtGrk2AoOoHr6sPCTKja8hrAcRsM/Wwhvq4iRG+NyQFdaloCx6h+Vir84dddo08'
+ b'fdNUy5hbxNgxVk2o9i6OjkizQ09izWgOFcX/Gahq9bcYYYUw9xqAXxjs2i3GHJjItQKzzSS0'
+ b'fC7tjUHkfGzNjsIJuBpfWFj6wN7dc29jqJJidraYBTOjWi1DAegyQ7E0cO3fWi1OK/bHfAWe'
+ b'fCb5iezh1wFs9iMYWFD6IwO8hPNGvhkA90p9SBtmr4aH94gsd9ZAZi3BfFISZ+FqzDceXG5/'
+ b'5dgc/cN0Wd/WZQCIC4AUmkxqplalM822ViztNlTqU5UVghBwgsAeaz3Waf3TsK9tPWwRE0Ce'
+ b'h2YgIpHDcNtIXWjq1zojRCEoel6wgCQeXGm9jxjOLHP1ErDS2xAwUSsrmzkoGhT7DLv1huTj'
+ b'D3PIxh/SB9B1iPrGFxZa30n5NqaKXGsA3l6UZpBwW4ORtGAmGDgidTno2rqe070QxwuM9zMM'
+ b'VvXMCx46tfhnRyZrT5SV2uOLZ6KuP1prNe6cgZrYdQxixB2clO+3416r1fd6EI6/fmDHuMQx'
+ b'PVGrfBRoDOFKNo5cMwAr/W16Lyn4feGFAxMZmwOglIsQuOagGawO6QcQ9q4HtmlT4DVxHFh5'
+ b'rN39w32VyttEdJeEghloEGGmVkGjbmDAvusvmH0/wNi4Str9wPCIH04DkNJfMFAvl0c2/i/I'
+ b'vQkgbZfZER98WDDE5KUYKDjp0k4IkUiDbBM+wm7fIeOrS+3fJuaOTfpxm7c6HZ5ZoVouK8Bu'
+ b'oCHEJNpbD/r4BobpLrBrLk4Q4oXjS+3PDvGSNoRcawCAEJ9tKGE3hXBppWyrywwzZEtWm1zC'
+ b'qScCiZ0qDKkoBlob367Xv3jjjrEHxlndrKCgJEqjbBSnycBstcJnWh3rD1Ci9Eeqv1ot6Ycm'
+ b'+R3E9hfBA6jPf2Z5+eQQL2lDyLUGEFKBQ/tl3yUYIQwok2LALt0U6Q442180AXK9CAv0xbeW'
+ b'2+8nZnSI0AEFs00BXSaMV20akDBz8bcKks090mz2cSFDrs/XngDYOTk20uE/Qa41AB8G9E7A'
+ b'MC++8w/b0J9IZF7lRkoHySQkmw5aOAH64cGFpQ9fOTX/nxXUblBvXgZDgbRSVa25RaRE+S+B'
+ b'fRXnKEl+IMw3OUHEDPz9icW/GfJlbQj51wC8zbX6UcJN3pbMIAQYhxaDP8BqBgX64wywcKLD'
+ b'91pHbWgZphxzbhJjplbu8eX05HxE95xHZIRrAxj8vU63O7LpvzHyrQEgJN+4ht+9E+E685DP'
+ b'HbNe5NSdRRzZ/mL/c8EB1sPXllsf2LVj7C0GXAITyl4vs3NUKZcAhMxOQvDzCEblDss67MKa'
+ b'oSWFf35k1ON/DvlmACRmgI3/+1RgDhWBxAkfssqkGNBpHiEDsdAA1sfDKyufbcxUP11T6iYX'
+ b'EQyZgcyoA2qmWuaFdldJDYDi0VNZ47kXE7Cu9F8N+7o2ilwzAEAy/5xc58g3w+z3BaDo81aI'
+ b'ZNcUVFqDF/R/bny70f2tJ09UbpLUWQ9l6+nHKmWcbXVtbj2c8B/BG2udy97+5+82un837Gva'
+ b'KEaNoZ4XYhvRuEVk7X4bX+569TFyDkZ/k9bg+DwczlO4ANfHQ3r5TwzxKfHVdJldv0ZnDiil'
+ b'KlpxV7QrFmY/VKd/z/A5KJDsU3z9W+32tzO8baki5xqAnQDNCqRszzlZICEl2JkGzgiwm1Bm'
+ b'oAEgMj14JAXVyOHMGSyc2o0Pz5fwRhC7PgHhztUNYaZaUSeabcRmQBZRnAuFFyru+URJ/T2s'
+ b'OyAXyLcG4KVvILrYQS/ef8eZXUsqyRdIccTfLecanTU60nhwufWbLBqcaFQAhMxL5RIpxO8H'
+ b'ghuFIVqo7P77xWY7F/F/Qb4ZAALR+15w6E0Ntr6A8GbfPvKbHPCPHDGgggNsBN9tND7bJPyL'
+ b'bOQqff0BAApYIdKT5TKHpCvZEGb4w0Z+guZHjPZMZWzkdv9ZD/k2AdyNVwrQogGAvVe260wA'
+ b'v2mjko0p0u8HELeB8rsDF06AjYAe6ZrfvrRSeoYwzSCVFAwTxislLHS6EGeh20Mo012ENgIp'
+ b'+gp5DPzZL9frjw/3qs4PudYAAGdrc6yOudwAhHwAUHAc+T9KcXD8tc4WKeT/xvEQlf7IMC2E'
+ b'JCprWsl9ZOcMtF724AxM25l73s5fBEcgAZiuVj4x1Bt5Acg1AxD1327IERZO8Mi6/H8E51z6'
+ b'lYCIvtd5s2EXRYGNYWFh4cwCqT/1c8jBsw4Glg1hulJWzEBX5hvDV/9jE4AZ+Pxis2AAg4Rv'
+ b'vhAlYUTlmFFGYEjXFB9BmsPvCMTsNQ/iwgI4H3yz3v6tyJaGz6Rw7QJKJc0s95sjXwuGNCKm'
+ b'75K/lqbmzUju/rMecu0DcIqi58gqkhyMkA8gtqL0oU/bdhSnlWge4owsWMDG8b1m8/4jE5XP'
+ b'VYCnAQBY2oHYm7tCrCbLJV7uGiUmwLCdAEGgAAr8D185geWhXtAFINcaABCcfj3SHk4LoKS6'
+ b'npXY4Mg2DSphgfMCP9al37FzF7oruy4O6BCjViqBETLvJO36Qgq4NjNCG/pg6jWgRr77Tz/k'
+ b'mgF4Qkcv8YvKbxDqAWLzILMh5/a6SYHzwcMo/T4TL/c4csF+R2HSSpWUYunBEDSvwQ5glQ+A'
+ b'n+jyJ7O7M9kh1yYAYAnOkLT7AcBinzFIuvQiCH5CBq2jnCoYmE9YKAU2jsXFxdNLc1MfngS/'
+ b'WqjN7bwNMFAnwnS5pE61upb44D4zBEsgzvok5uMPdzpfGPxVbB651wBimz9s0mkJkSjK2uJe'
+ b'B06qA0EbiTPVCpw/Hm50328YZGT+WMwpu+mm1pqBqGcgMrLq1hkhm9S+Lmv9d0BvPVNekG8G'
+ b'4NXuBCEi1AP0euZTTgGOB3s25M9V4PzxSLP5yS7jK9JfIdRV2scVZjVZ0iwEKI+DgtckIzOl'
+ b'rVSu0n9j5JoBAMHu90QvahmF5+w4dk89QIrD+O8MUQDmIgZwoXjCmN+OtbmYmXaYUStrMHq3'
+ b'Ezcpz+nafp5oj0nLfLqPGc5V+m+MXDMAScGMF4rVAoIkjlU3j7R1wj5fW+DCcZr0HxBz3cf8'
+ b'Ye+p2PpGKVVCJIWZYYsx0p7YPkMczOHc33i41fpW9nclG+SaAQC82v7j2CTg8Bn3Zg+DSOk/'
+ b'QBZhIP6guhY4XzxRrz++ovRfSmZgb5UgsEyMyXKZvScewc8ziEhP7HcYK+mPD+cupYNcMwAG'
+ b'eir/fFZWfNx/Ej02Y7ojrjWI2UKBC8UTzO9lT3jx/QUARrlky4VFEktb+Czlv2R9GscIGIAB'
+ b'/jrbO5Etcs0AgGDfx9I/dJGNF4f8QfojDjEWiUDp4KGF+t93wV+Ni2+EAzCAZWY1UVIsNnkv'
+ b'g8gWYv8zuH5Stx8Y0GkzQa4ZgPT7C0lAkoQTGIP9ICJtIMsLguc0DBRewM3BLJZKH7RmQO98'
+ b'KlhnYLWkI9WfV62D1P9zAsXzfubPf2MZI7/7z3rINQOwEJU/aABy1JcDJz6XdgjQn989R6EB'
+ b'pILjTfP7zFhmDjsECRQA1kqVAJZqUO8PSNPmj0f8/QB2VEsfG8JtSRW5ZgAMUbcpaAOIk4H6'
+ b'OQYzGtF3i31YYHM43Wg82lTqIz73380rADAUlgxjsqzBbEuwpVNQFvDCQ66Bga812wUDGCqo'
+ b'l+D8Ft0IjKCXULMh/55oA2QRFiwgDXyvbX4z5AQIk7ctnhgMrTXHZkAUn091cELbM8AJ7uLz'
+ b'g7sT2SDfDADwan5PDgCjRyMIJJqRFsARuXNgNQU2jydarfu6rL4uyT5S/Sc5ASvMelwrHxIU'
+ b'9TxtiIkpWkaZcf9xYCWDUw0UuWYAQfpzZA4It3ZkGBMn+pgEaQz0GQUHSAvtBcbv9ajgkDlV'
+ b'6DBQK2mv8Un4l9aalwsYsq5sBaJdbXvGS7nZ/Wc95JoBxMp2D+EzfHdZ3ysQkamQ0UgWHhVB'
+ b'gHRwhtXvEaPhid9xXSkCJAWl4ZyBiIQCpzTAPeuIAf5co5XL8t8kcs0ALOFH4R95BMAgGJH9'
+ b'SbGf+oWs/u5CAUgPpxuNRzq69BFitlu+w91fd8uXiDFZ0srX5yMjBs/eFPjW4x18fUA/P1Pk'
+ b'mwFw0lEjExXCcYiOy4LJxASIzhU/FkgHx1ut9wEIDV4BsPItIKC0Yq+FiTBASoMj4gdQVvgk'
+ b'crT7z3rIdUOQkJPNYBVtDhpNWOwQkp4hae8ODPSoh5k4obY7TrbN3+wvl76pwZcb1yFEqcAB'
+ b'Ggw1rhU3ia0mALtl3KabhXhGEhy7rDnX6b8xcq0BAGLrwU2U2GzyZsTHufev0hyxpIgz0Qqk'
+ b'is4i4UM+wsPwZcKKgTYDVaWi7d96NbMLhWXorvMzAURon2jSP8xQ8hwAABBASURBVG3yt4wM'
+ b'cs0AeuwzxGpflAzU57NZjZ5rK+g/dZyF/iAB7dUFWBZGKaUR9epPMugLGhyYOwDF/KXvAd8d'
+ b'xO8dBHLNAIAo3CMqPyOyzdk/B1K2C5PDLzis5gYFUsHZZvOhttYfswweoUDIqfl1YkyWFIvX'
+ b'PnLabcq/4/sOgDFeVp8Y3h1IH7lmAJ5DeyYQ1wUEKqTos9lQZ/j2WP0skD6WSL0vLv4J3Zdd'
+ b'azCt3DxwtIPQhf+XLPn+dtfkPv03Rq4ZABBJ+kjhC2pb6BfH8edTHuDo+xOPBdLF9xqNvzLM'
+ b'D0mzEJkDBQAMNJnVmM8M7DUDzxdBs/SCZXna4F/S+i2jgHwzgH4qfeJYPP2ZxYd7zhfrAwUy'
+ b'QKdTrX0oZH+GO60U0GLrDBRtzH8Om5hbt6Y0+IGHgLOD+6nZI98MAElpHBw2RCEisJo4sx8F'
+ b'ssOJdtc6AyMNL97twSilFOJNY2GFxQWMOPW4CbVlwn+CXDMAIbSeZKCI+norA7MldmE8RRAw'
+ b'eyy2Wt8krf9OpHzcGlwBqDNjQttsD7ul+PlrAHFymXz3Upf+dhC/b5DINQMAZHLYU2JYFFgt'
+ b'jjl+kt7gnu92l1NkA2WKJZR+0xJnVCUIgKFAsBuIBEKO3XobZeq95eQEHJ8DvjTQHzkA5JoB'
+ b'sBO7IeQXT7EcC1qBSOnUa8Vj7YLD6wLZ4bFG46+I8V17z8OsS2rwCrOu2s2EfCPP85kTkf5S'
+ b'/qvBn3oQaKX7K4aPfDOAfhxdiDFyDsVqHSP5B5sf/UyCApmj0a7U/sCnfXOYbZsZyDYzkIMp'
+ b'uFobWCv0FxyHkldCauvZ/0DOGQAQlPCkFO6x+SItINNrSTCDohw4W5zqdD5I4Jafc7YmAODm'
+ b'wJUCiFnYowmuM4BVa4YXOnTfIH7ToLE1GICYAO5ImMTYGedZRdoKQOJbC01gUFhut79mtP6U'
+ b'36jTJWVI0dcSA5NxlSDgewquh9WhZHz9BPCtTH7EkJFrBrDKsxtL/sjmS2oCm8kM6/cfuHfR'
+ b'FBgcmqr8XkvgEWNXgOwmDLW6UctGIkJxNWkF/HfYokWeuWYAQJDywfa3BN5bHLSaq6c5eirP'
+ b'Cg4wUDzWaPxvJjzK3N/R1wKUOAP9HGGd+eTeeQUAVdK53f33XMg1A0hOmsx+8AcNXiWPGUKB'
+ b'gaBuqtU/ElNMQnfKzUCTgZpkBqJ3zfRDH+2xfbpt/jHj3zA05JoBxAi2uHvN6xBi2ipA4qux'
+ b'+nCBDHG6a95PQLenGjRyBhr3OdfOO+wtwP1HnASkGJ95HHhiwD9pYMg1A4i9//Z1ksP3pgOn'
+ b'Hf9PZosFI5F7qhELZIvldvurrPSnwtzbZ+IMXAbUuFK8an2swczjeR0v8ZYq/00i1wwACJLf'
+ b'Z215go8885HdF/4mIyWAA/MpMDjUdel9QMIf48CwhUI+ZwB95g295ptoA492+G8GcPlDQ64Z'
+ b'QJIAQxmwe82xabCGzp7ilYQkkqIWYNCoNpt/ZpgftyXg3DvTzGgxq4pzBiabyK4zzi4Aud79'
+ b'91zINQMAep02sYe3d4I5ew+9//7E4iswEBwHVlB2zkCGN8G8MxBAVUERog1EEmKhl4EzNPM/'
+ b'Ygvs/rMecs8AkqTG/t/+Ujg7+V9g2DhjzPsBcCzh4zJhUoolMzAIibBi5FGchPMV9dFBXPcw'
+ b'kXsG4OP+kRYQnDy9ZLnp/nDrDRRMYNiodzpfIq0+5YnbhwTd+8zeGZhMBgLYV5C6eaRvtmlL'
+ b'7P6zHvLPABAmMFji0eR6s4B7Pp/NiNT/ghsMBW1V/gAQJLzMg5LXKqkBhH4CCd/AowtbsPw3'
+ b'iVwzAK+2JXwA8oYcj7WDTDkAgpZBGEgxkNrA2FY40Wrdy8CpmCnHaAOqDFA8T4KYKWjGfdgi'
+ b'u/+sh1wzAKC/DTcsdTzDc6ro8XyJe7sxgTqVy38UqgMtpGlog4GaUiom9j58HKyGvvuPhr1s'
+ b'ecyEueebAcT2NzsV3D/2C8f15gikVgyE1ZImBSQneiPSfrsRe18sGP4AXCvAVUwAgEmUCfue'
+ b'AcEv0FogDHr3HyF2DaAUPe83t6nJuFwzgITG33O8390R4k8/G7CX/C9wZuKJ1onn/RbDWgtj'
+ b'9U/eZljpdD5LWn16lWR3WwmuMDCm+luEDv+2MJjyXwVL7OXEKCHM6zqXuXnkmgEA66v+4hPo'
+ b'DQtldx2rM8zOaUImCV4nRqnPsZgZyKnjR6zxeluhpfX7g65n4TUAMOB6B/YbYwofz+iyYoKv'
+ b'RqPijsm8rmWhpI4twQCSFJCsDxjENfjnHB1bTf/9pHoJYVEkpUEpGkmi76d0DNMFMlI40+r8'
+ b'CQGn4z4A8U1pM6tKH7+whU7L/pf5FoKvuTEOYMwdK6OX6LuA39Qo88LS3DMAAIB0/uHVlIDE'
+ b'67TuZjAnIpOCV31/P6kuhF1ZZ8SE38/2kxEvkG1P9AmcRaXyP4HgGwLCzXSZgWAg2kIMALBc'
+ b'N+YzmzivzHkFlsAnojEJS/yV6FK6iTEQwheUB3GSrNBL1Fat63X8Rc/t2/7p5s6aYDQJxiOh'
+ b'JQqSPCn513PcxYSdLDbsa+ls6udsYSx26f0zwOsBlERAAICCAisG2RKhnhuqgM89Bpw4z1PJ'
+ b'3IqmVkZg5GtJ+I57jPnPwOcy1wxgPSRNAwkDAVhNcuf8stVEH5+jnzgGAF3COAwmE2eNGQAS'
+ b'fx4TvMFqRlBI+/NAo9t9YLqkPw/wUQJDQ/Xc+DozxgFeiVYEY8PhP5nH2GSroteMkzmLJXwn'
+ b'et5v2QwUuWcAnioclcfdgQi9No7c7Y3Qf7Kefy1zop/JwQxoBTBKs4CZQy8hxx8XIjeJIccK'
+ b'ib9JdJR+f5XMUTHZlAsAKrY3WCOsF6WABq3pAIy1OCHyCqxNL468WLU3sETeAtB2Q4h/oGr+'
+ b'esg1A4ipSSE44OSY+AQSKt6a4HUkvTyuPsY951VaLTcVPrdI/PGzhhYB7II1Oduwi0IeRQqY'
+ b'6DGpDhYSf5MwExN/zMtL7wGwQ5iAQAHo2MxA7tqXj00AX17q/YrYWStEPuaGOPVK8Slhid7A'
+ b'zrvMfWzjj8x85poBAD2mPWJ7rp/EVlh959eS9GtRoLCInj/Tqr1CeGiB6EtLXfo8gNMAFgFM'
+ b'IUjzFqwEiCVB0vFTOPVSxuLi4umxavXP0e3cI7UBKpICKwBmAFUHoBkPHLeHYntenHk1WEfe'
+ b'GIIXX+x7se0bsPNcj54L8Y8U4QvyzQAchbJa486y1QpUQuyfD9GLky95BqWUqQNnFgx/b7lr'
+ b'Hma7bfRZBC+/lAM0YBfECnqlwVoOoJFbJHnHEtF7p4B7gN4CIX/jFUgDmsEfAXt1vgZL5OK9'
+ b'F0/+OELMvgs7p0tuLMLOcwNB04tNv5FDvhkAnGR3TMDXenOQ1EErUJ6yYk2gP9HzanMCgFaK'
+ b'loGVReKzy11zhi1h12E5/QosAzgF60U+icAUhAHE0qCQ9gNCo9t9YErrzwH8tH7OlDpDTwP8'
+ b'BOGzAPbAEvksrAY3DcsAarCMgWGJfhHAAux89yP8kSX6GLlmABuhmphBrGUaWF8B96VGpRTV'
+ b'Gc1l5uWlrllmS+wNAMuwC+AM7CJ4AoHwT8ESviwMkfzJcF6BwYC6Ff1bZcM/CrckHHUqBlQX'
+ b'0Fqpaoe6ZQDXwBL/HKzEr8BK+xYCgxcmL/Mr5t1IqvnrIa/FIwoAppR6cEaryxSs1x1w+hZb'
+ b'HXxsrMZotZQPuEe2gO8ajNUzphRoidFaYdQXDdXZEm8TVpIvw068SPeTsDb/Kfd4BlYdXHZ/'
+ b'00JQ+QvCHw4kXCdOvCqAiUqlMt3pdGYB7K4AOzvADCzhj7u/EaIXxv4Y7PwuItj4I+PRvxDk'
+ b'UQOIEmik44tKOANXS3QFQMW7PSa/VCleYm41GPWFLq04SS+qfR3BxluEJfSzsIvhNKwmsOA+'
+ b's4Kg7kuyRyH1Bw9ZJ3Huvaj2cwB2dTqd3QB2A9jdsap+GXbezgA4Dkv4jyNI+yVYph6bcblG'
+ b'3hhAv4y6oOYj8tIn/rAf5SmleJm5vcKoL3TNCod47QqsBBdpLxL/DCyhn3WPsijE/hNVP5nl'
+ b'tdYlFEgX8fqI4/TTAOYB7ASwH9bO3wXLCGqwBL0E4GEA34OV9I/DzvMyVtv2W2Yu88QAknXx'
+ b'ZbZ0D4BBPcHAROZfdFwr8DKjvcJoOKKXUI146+sI0vxsYqxF9BLeE8mQ9P5umQUzooiFgoTs'
+ b'xgHsgCX8iwDsgyX6eVjvvoEl7kdgCf4JWKI/BTvPSaftltTg8sQAgF6VbkyJVh9Re0/EzvEE'
+ b'rYBlVu0Gc+NsUO9juz4melHxRdKfxeoQTz9Jn0zdFWy5RTNCkPVQQgjZTcOq+UL4uxHseiD4'
+ b'aE7D2vVnYFX82G+zbXw2eWMAQvzjAGYY6BDQhNsFSkX+AQ2oJpRZgVo50zENDp7a2Jknnvx+'
+ b'Kr6o/mLTi09A8rmTJZtJ1XBLL5whIrbtJRNvElba74BV83fCOvTG3ecWYW36s+hl7uLMa2C1'
+ b'v2ZbIC9RgFi9E9VuN4CLARyEVe+E28/CLggpzJDKKwndiV0fS3gZot5L3F6IPs7c65enXxB+'
+ b'9ohte1HxRdrvgJXyU7CagIadIyFuMd9Eyot6n7Trt93cjbIGENv8koctxL8T1pGzA5bYZdLF'
+ b'gdd2fye592Lfi7Q/gyDtFxAYgyyYWNL3S9UtiH5wiPPwJTtvGlbCz7rnwuwbsPMpOfiiwYmZ'
+ b'lyzI2ZZEH2MUGUBcsRlXXo3BcngZNfeZJiwxd2Anv+qOM0KqZh1B6stjLA0kXt9B/zrtfotl'
+ b'Wy+cAUCkvbTNklTcKYTMvDJC7n3szI1z8kXSj0T57ahhlBhAsimmOHeSFViSjtmCJeY2LDHL'
+ b'gtAIxRmiEYgEWEavei8e/KQzT4i932IpFk+2iOvp4/ZZEwhlt3ElZQdhLlvo9dWMVOntKGLY'
+ b'PoBYze/XIy/O3hKHTzV6jDuvSPsskfyi7ok6GC+SfpI+lvLJBI9i8WSL2L6P4/cypMBKyvdl'
+ b'3iQaI0QvpdWFer9BDJoBqMTzZM+8fo0x4+aY67XUkrJMGULgsS2/HtEXKv7gkWywISp/sjei'
+ b'zK3MpzB3ceIlNbcCG8QgGUCSWJOtr/t1wY074QKriTXZYSf53lqjIPrhI55naYst0l/WBxCI'
+ b'O2bgcTk1UMzbBSNrBpCU+P363ycJXkef70fQogKeq44+SeAF0Y8G4jlPtkOP+yTGhC+Pazlk'
+ b'C1wgsmQA/Zpg9mMA/VT5JOHLgkguABkqekyi32IpFs9g0c+5G6v3YtsLk0/2xi9SqjNCmgwg'
+ b'Ke3lMSb85Ov4s/0kddIxt5a03wiKxTN4xFl78hgz/uRcJ235Ys4yRloMoJ9zT57rxPHkOddT'
+ b'0ftx/mJRjD7iuY53NZLRb7+DIlQ3BGyWAawn9ZPHkkhK8372fLEg8oP1tD5BP82umOMhYjMM'
+ b'oB/x9/vO5AI4n+cF8oEk8a/lzykIfsRwIQyg399s9Hv6OeNUn+MF8oN+BC8oCH7EkRYDOB8U'
+ b'C2LrYK21UMxxTnChxLyRvysWQYECBQoUKFCgQIECBQoUKFCgQIECBQoUKDBM/P9EbuQz0a4x'
+ b'6wAAAABJRU5ErkJggg==')
+
+
+IPconvPNG = PyEmbeddedImage(
+ b'iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAmN0lEQVR42u2dB3wcxbnAZ7Zc'
+ b'P/VqWZJtyZbcOBeMbSBgjAnNjxrAQEzHlIDoCRBCEhJaCCEOyeMhCPBCKIYQHiV5AUIJjxYw'
+ b'RoAxRrhItiwXWV0nXdud930zu6eTYuNbSSvdEb7fb3XS2dLNfPOfr83sLCXfyL+10LFuwDcy'
+ b'tvINACMsr936g8KOpg0XmT9LGTn0W4cteXPT1m1vzrvwam2s2zdYvgFghOWJH3wvcMyCeXXm'
+ b'z1JmNtMlB5U83uZN6z58pqy0fNXGzQ3v7X9BasDwDQAjLE8CAEclApABAFCFUslQNaVEcnka'
+ b'N61b86fSkpJVGxubPgTLoI9Ve8cUgNCHb1FZohczRmSGjZFliTlcf3ZOndU0lu0ajnAA5s8d'
+ b'aAGoSqFvMPaUMOgsfCFMhzGXJCI5nBvqPnr/aX9O4VMlOVkfFxx5EhvN9o4pALtefeGwvILC'
+ b'13BWoFKIy8M0TbtArQ48NJbtGo48feMVgSX7BwZYAOCbEkUAgMLMr7qAgb9KMqOKvL5hd9tT'
+ b'erjvqdmnn79uNNo7pgBseu7JJyZMmrSMoHnUQAm+DLZ5a9OqikOXnD6W7RqOPH1jTeDwObPi'
+ b'AMhoAXDowQLgjB8oTNCAFoH1f48wSA71k7a23U/rfcFVFctWbLCrvWMGwOfPPp43pbxsC1Ed'
+ b'bt4KqjDqcIECtJZ/fvxJ8cJTzhwzvzgcefoGAGDuQAugIwFoAaR9qJsbBMMqwKXDL4KL+CJ7'
+ b'8dJqu9o7ZgB8+cwfr55YUXE3BAHgDxmhbh+jjPH21G/ZMrf6iGPXjFXbhiNPXX95YEkiAJlZ'
+ b'TIsxKikKDwCtCPhDZKI+96iTq+xq75gA0P3O32koGFybnVcwjZt/nCIwU2gsytuzYWvT96cs'
+ b'OeausWjbcGXV9ZcFlswxAKBoAQCAqA4AJGEBEgVDA02HF1qfd8x3vl4ArH/qkW9VTJr4JlFU'
+ b'Ye6cbiZLECihguDHTU1NL1cefsyRY9G24coqyAIOn71fnalZGcCORWPCBVAp6b/DA0VuAWh9'
+ b'/tLTvl4AfPD73/5h1qz9lnOTiOYfZ7+uUZ4mcfRJ7+ebN+fNOObEvrFo33Dkye9fCgDMrOOq'
+ b'pQYA4QgAoFi0AKAJjSfH9fnHn/71AWDL80/mFObmbKVOp4e/IalM9vqgwzqNp4MAxa72jiOK'
+ b'Dzrs76PdvuHKk9deIgAwNCv5EYAwtwCSZMECMCMGoFJ94Ynf/foA8Pmj99dMmjx5pRn8Sd4M'
+ b'JuHUx9nBgyTGU8LXPlh95xHnrLh+tNs3XHnimosNCyBEhhgg2hcysoDkARAWQEed1Bd+55yv'
+ b'DwC9rzz7seT370fQH0Lwp+TkQ/AXMwAgwvkBGG1dXWvyFx42d7TbN1x5/OqLAotnTa8TioVu'
+ b'+bNYrK+XUlWJF4KSFW4BmFRfvOz8rwcAnz5078KKysq3QRno7Al1eZiiqBQLJDQBAF4m1Vns'
+ b'5ffeH3fMistaRrONw5XHr7wwcFgAAODxjGEBgkEqqdaCQBQ9poFOpPpxZ6z4egDwzsrbfj9r'
+ b'1uzzhPmH2Z+VxySicwC4+Y9bAA4Aier6ae7ZC58azTYOVx6rOT9wGLcAJgCZLNoTNNJAKwAw'
+ b'AEC4gJLll6Y/AB898OvMyeNLmiS3x8c/FYI/NTMb7RzlijFbYlbDAII16+sfnL/s7AtHq40j'
+ b'IX+8/LzAYftNrTNTWswCIl2dYAEUizGAWQii9aXn1qQ/AB/fe/slFdVT/5NgTRyXR0AxiozB'
+ b'H9bI6cCmMF1YAUIaQuHIJP+Cw0Z1hWw48uhl5wQWzZxaJ8w94y4g0gEAOKy6AGEBMAsov+Cq'
+ b'9Aeg4eF7P8wtKZmDwR+af2fROFH5k/61Qsa4G9AQAvblth3V048/tX602jlc+e9Lzw4snlld'
+ b'J1JaEQOEO9p5DGA1DdQ13DMi1U+4+Lr0BuCDX/543uQp1e9Tp4P3TPL4mNPtFnl/PP0b1Hu8'
+ b'IA1a88WXly5YfuF9o9HOkZD/vnh5YNGMqjqzTzJkAZH2NpEFyNZKwSwm6gCTvndDegPw+s1X'
+ b'3T9r3gErePAHg+oqKGYyBn8y+v69zAp0Axojm7fvfLbqhGUnjUY7R0IeXnFmYNH0qjrTqsn+'
+ b'TBZu3Q0uAGMAOem/w3gdAC0Ara+44ub0BeCdO3/kn1xS3CT7MzLEjFeYp6CQYOmXA7C3JhiB'
+ b'INNZe31Tc+F+p54VtbutIyEPXXB6YNG0KXV8tuPSPgLQ0kJlAIBYzgKEC5h87S3pC8Dfrrvo'
+ b'grkHLHhAwtkPClFz8phThdxfkURhhO4dAF4PACvQ0t2zsPTok96zu60jIb8/77TAIdMm15n+'
+ b'XvJlsNCunQCAank1UDMsQPX1t6cvAOvuuPGfeRMnHYApEA6or3QCo5omNknuQyFYKsY4YFPz'
+ b'9punLzv3Z3a3dSTkwXNOCRxSXVlHZQGADAD07dxuAGB1LSAGHEj10266Kz0BeOO6i2ZPmT79'
+ b'Q8np5J+j+DKZ158hyr5m8WfvKhB75QCa9u7gm+OWnnKonW0dKak962QAoKLOtHgcgB3N3AVY'
+ b'XQ4WQSCtn/GTX6cnAI+dfcpvFy0+5Hu4Hw6DP19JGVNx5ccM/vb16Tozy8Lh+m3b8meddXG3'
+ b'ne0dCbn/uycGvjVlYh1W/kwAepubqOxEAJIPAhEBLSZcQODW36UfAC9fe4m3srCgyZGbnYUz'
+ b'ncoqyyotx+SWW4BkF0ZMN7C7q+vY8pOX/9Wu9o6U3Hf6cYGDAQBZEYMtef0suK2JKg7rlUAt'
+ b'FuNrAbPvvD/9AHjpnNPPqZo/62HcCIEpnTu/mHk8HjH7B1f+vkq4FdDIG3VrVy79/s1X2tXe'
+ b'kZLfnbY0cPDkCXUy5P2EL3f7WLBpC1gA1VIhCM0HWgCMAfb/1UPpB8A7l1/4dlHVxANxwHFv'
+ b'W97kqUzCXT+G+U/GAPD6L08FsSxM1/qPOnmmXe0dKfntKUcHDqoo5wBwF+D1sp6tW3gQaBkA'
+ b'Yy1g/m8eTS8A/ue7p8ycOrPqY9nt5EtiamY2y84vEFZfliytizNzY4Su63//6NOyk3506za7'
+ b'lDES8puTjwocNKk0DoDk8bLuLY1UhRjAaikYXQBagAPveyK9APjPY5bcs3jRgislCP5wb3v2'
+ b'pMnMpSrGzREWP9JYHkYrENX1s/NO+O4f7FLGSMjKE44IHBgHAIbP7WVdWxogBgALYLEUjC5A'
+ b'hxjgkNqn0weAZ849zV2ek73Vk5+dy2e6rJKiKVMZ1WOG+bf+kSIQ1EjdpsZHF11z01l2KWMk'
+ b'5J7jDg8snDC+DgdcAOBmnQ2bqWIxBmAJQeCih/8nfQC4/6jFZy5YEPijrIgZ4BtXxjIzM/jg'
+ b'033m/ntThlEX18m21Rs2ln37+ltS9q6hu5ceFphfXlLnUFW+w5m6XADAJrAADiLJQ3ABlNYv'
+ b'efQv6QPA86cd/4/SqvJDkHYM/koCc5lsbPqwtiNmoDZ0XhbWyaZdu2fOu/wHa+1SyHDlrmMO'
+ b'CcwvG1encgsAHs/pYh0cABXCHyuFIEZixmrgkU/8LT0A+P3Ri6dOq5q41uF1StBy4szOZUXj'
+ b'SynfIm8x+BugDGOrOFqBzS1tV8+74oZ77FLIcOXOIw8OzC8trkOTz10AArAZAOBBoIVCkJkG'
+ b'AgBHP/339ADgyROOuqu8Yvy1WATB2V9YNY15Xc5hmX9TGdwNgEI6Q+G/TL7wqqV2KWS4cscR'
+ b'BwbmjS+qU00AHE7WtnkjdTgdlgtBMcMF/Mez/0h9AB48bomzxO/bmpmflc8XfiSFTJo1h1Et'
+ b'Gj8cYTifxgwLANlA94aduwsOvv5nIbuUMhy57fAFgf1LCutwwBnTDQA2UdViHQBdgBaF/kq0'
+ b'/vjn30p9AFYtPfzUgvEFq8wKWFbZRJaflyMGH/3/MP8+M5aHsSbQ2tt36IyaG9+0SynDkZ8v'
+ b'OgAAKIhbAKoKC4AAyJaCQBED4H6Ak/733dQHYOWiBa/OmFy2GDsZg0GqmHsAc6ABQwBkC6Xf'
+ b'vWskbgUaWlpvPehHd9xkl1KGI7ccun9gblF+ncPlEAA4HKwVXQAPAq3tCBIuQKo/5aX3UxuA'
+ b'm+fNnDyvomy9y+uS0He5cvJY+YQJfA2IKonmf5gfBxYAd8p29Pa9N/O6ny60SynDkZ98a3Zg'
+ b'DgDgBBegGwC0bd4MFkCxaAFIHIBlr6xObQDuOHDO7TMnjrtegdmuwSCNnzaDZUDwx++GQeop'
+ b'TRj/oX+kWQ+AK/bml42Fy3/3UJtdihmq3HzQrMCcwlwjBkAXABagsQE4gCzA4rZwMw0847U1'
+ b'qQvA7QfNdhS6nQ35+dnF/HYoWSHT58yF4C9Gqbnn34z+4yAMIxvQQSlgBeCvnzT5mp8+a5di'
+ b'hio3LZwZmF2QW+cEF6DrJgCNAgALFqA/C5Drl7/xUeoCcOu8GSdVleQ/o0Lwhx3OLZ/IinJy'
+ b'+K1QUmLt3xh0Sgf+bEkMC4CbJd/Z2HjfWQ88dqldihmq/PCAGYFZBdl1LpeTuwCiqKx1SyOP'
+ b'ASSL6yBRYzn4nP/7JHUBuH5G5UtzJo37NgY4USB26vwDmVPX+m+EMD9hJKwArwiK3bIQEH5Z'
+ b'/aNfTLFLMUOVG/afGggAAE6nU7gAAGD31q3CAgylDiBJ9ee9tTY1AbigonTigrKCeq/bpWDe'
+ b'6s7OY1WVlTC24LlkuZ/4RN9vfG89JhB3h+maAEDXNPbJjpZJy2ofa7BLOUORH8ypDszKz+IA'
+ b'6HhAHLiAlq1bKK4NWCoFIwCaCAIvfHddagJw2ZTyn80dn3cTBn8xMM0TZ85iuT4PHvjJzf9e'
+ b'Z7rpDoiF4hATxREsrmAmgAslTd3BFUff+/ADdilnKHLd7CmBQC4A4FKNGEBlLU3bqFO1lgVg'
+ b'fyMxUQi6+L31qQfAJVNKlXyno6E021eCA6lJCpl/8MFMxsqfsq/KHyX9Y58EBMatofFjVjUA'
+ b'IBojn+3YteqMPzyzzC7lDEWuCVQCAJkiBsC6haKwXSYAliwA45MK9wNctro+9QA4u7zouFkF'
+ b'mc+pkkj9iisns8qSYpj9soh2E//y4Ht7E71B0gAwcdOosTKIAGgxrWVLd3DcyQ8/FbNLQVbl'
+ b'6v0qAjOz/QCAQxSuwALsamo2ALCwE4oYQSAAULNmQ+oBcHxxzosH52cdizM9DA096PDDmZ+n'
+ b'/ZIR7CTc8E/+lYH4hye5OZAZx6oyIxXElTKEoFNnc4+8/7GUOVTyyhkTBQBGIQgtwM5t2wUA'
+ b'FusAEeOQqKvrNqUWAAdm+0sX5vg2ZamKgp305hewQ+YEIOPD4E8xxnSQCWDx7wZ+eDKBIGMG'
+ b'ACy+JoAARKNRsq0reONpq1683S4FWZWaaRMCMwAAtxEDEFlhO5p3WLcA8KtRA4BrP9mcWgAc'
+ b'muP78fwMz08U6FAU/PHs+fNZeU6GMP9G6kcT//RAYxCHIDkA+k0/SwRAQwsQJZ2R6KvHPfb8'
+ b'ErsUZFUury7jALicAgAmywDATg6A1V3BUSMGuP6zxtQB4Oi8DFnW9I0TXGo5P9JHUclJxx7J'
+ b'5EiYr/tj6seXfsyBpSQOxJ4+bF+bRHjgZ7yaF8YAeHhCFOOAqNa3Ldibf94LrwXtUpIV+V5V'
+ b'aWBGlrc/CJQl1rxtF3Wp8hAKQdBPSut/+PnW1AHgmFx/tltRrpIJm+KTpGri8ZZPnlDqyPf5'
+ b'WI7fo+T6fRplulvHo9/0/q174kZg434A2n9nUFIADJr9vBgEAMQAACyWEFX99rFPvPiKXUqy'
+ b'IpdOGR+YlukdYAG2b99lxAAWg0C0ckSq//EXTakDwGC5cmalGopqRTkOZWpQZ1Peb2yurs7N'
+ b'KnV4PNOzMvx5OX6vUpTpl3J9XuZzKA49HFak+KlgdJ/3hw4M/nQ+++O1AC3G44B3trX88s4P'
+ b'Pr3OLiVZkYsrEQAPACBK40ySWfPOFupUFKJYzAKwDqCBBbilvjl1AfgquXVu9VVePfIrHHAc'
+ b'wAnT92NuLUbl+NkASSjCeJCCbtwboJsQ6MwIBGNEJ/SjU/7yxhw7+5KsrKgcF5iW4a5zO8Vi'
+ b'kC6BC9jRChZAthwERvh9AbT+5xu2pycAN0yfVDPOKa1UYMA1UEZJ9XTmjIYprhtIezobaI+K'
+ b'EKaf3yRhBH8mDJomsgF4T1/T2VN89+q1u+zsTzJy4aTiQHWGp84DLgDrIwwA2LazjccAyRaC'
+ b'mNHvCI8BSP3tG3ekJwDXTBlfU+5zrVQVmQ9WcdVU5giHuQWQkrxD2PT5sahGOiLRzY3dfYV5'
+ b'Xrec43bGqKZ5ouEIjYbDJEzY6Re9/v6TdvYnGTl/YlGg2u8BC6D2W4CWVuoCHSQbAxh1LxLS'
+ b'hAX4xaad6QnAVRXFNeUZ7pUOAwC8Q0gNhcRaQZJ3CKO/R3OPK42f7mx9pKG14w8aIVWvb946'
+ b'Jcfr0Ysy/Epuho8wp+udJz5aO+anip47oVBYAIfKF4M0ChZgV1s8C0hG4dwC6P0xwN0Nu9IX'
+ b'gDIAQDV2ChVWVjO1r7cfgCRdAJr8KFiAT1raan/+/qcXJfHRYybnVJYEqlyK4QIYzmC2raUd'
+ b'gsB+F7CPuDfe7zBmAUyq78wvWpZfVDRjs+SscuhRjyMaYSzYLauR0K4HPvj0tuG011YArpxU'
+ b'XFPq77cAhZVTmNQb5ADISQIgbpPW+faoT3a31962+rOUBuCsivGBKS6JWwCEXgcL0NzaQZ1y'
+ b'vwtIFoAIxjmyurtkwUIS6e7y97XsjPUF+1zdPUE52NtHunW99oXdXcPSh60A1EwqqinzuzgA'
+ b'uFO4oAIA6OnpByBJQUXGICD6tLWj9o4P16U0AGdOHAcAyHVuVeYWAIPA5tYusAASSfbmYDMG'
+ b'iGE1ELMdnAS6SImxuhwDfYThDdXpPOXR5t1/Gk57bQXgsomFNWU+dxyA/EmVjPZ0UwVdAE3e'
+ b'AuB6A66MfdbWVfuLjz5PaQCWTyiaM16lHyoS3yBNnKrKmtu6qAsfiZTk/RFmFiDujIf+4ysT'
+ b'g48wRECXvZoe65CV4pd3te8eTnttBeDSCYUDLED+hApGuhEAamlt3LQAn7V31f7y4y/GHIBr'
+ b'Z1bS5hjLnVxUOJlkZE7dTpWqspysMr/XU82isbJwV4db6+pi4Y42KdTWJnV3dDoceHsc2bcL'
+ b'QEkEQDw/Q6yHaPzSSZ8AYPWf27rnDbcvtgJwcXkBAAAWAG8WAXzzyicw1tlFsS5g1QJgDLCu'
+ b'vbv2nrUbxgSAK2ZW1PQozpmTCvKrqM9f5cnI8Ic7Oki4s1OOdnfG+nqCrt5gnxSLRXib+Yn4'
+ b'eBoePjXEeCJOsoo3XQAffNIPALeEcPWBLrp1dvuL7T03DrdftgKwAgHwueJBYE7pBKZ3dhgu'
+ b'IMlzgnjHdW5B1nf01K5ct3FMADivLP+xcq/rDMxe0Ld39IVZezBEEW7c/CZ4piSe3NKElG9P'
+ b'u+K+qs/GF2EBhOk3XQCuvvbiSepUPvyp3Z2vDbdftgJwQVm+AAD3DIIZzxlfxrQOBMDIiZME'
+ b'wLxPbn1nT+1v1zeMCQArJhUvGudUXkf3hX45HNNYU0cP+HaZKAll7cFd2tuDDpJzA+Y+SPFz'
+ b'3P/rek+UkYLn2nqG/Vg9WwE4rzS/ptTrXIkRMOiLAxBtb+cuwMrKmMb3x+mkvitYe98XjWMC'
+ b'wJnlhVKxKq3zqXIVmnQciF3dvQz6RR17qWoOdfATfz8eCxj+PwSxUKeuv/SX9uBRI9EvWwE4'
+ b'Z3xeTakPADAsQPa4UhZBACRqpIHJxcS8FAxXfVdvbe2GLWMWBJ5dmnfteI/zLhXMPprinnCU'
+ b'tYIbcJtZjQVJPhvotwKYFvIAUGfXvtDec/dI9MlWAJaX5NWUgQVw4GIQWICs4nEsDACokgiM'
+ b'ko8BBAAbevpqH9zYNGYALC3MKpiW4W50SLILBwUgADcQJCr0RE3mDui9uIl99V8AIGoCfVgW'
+ b'p2T2c63ddRb+zL6aZI+cWZJTU+pxgQWQuAXILEQA2uIWIJnKOEuwABt6QrWPNDSPaRp4eknu'
+ b'qlKP41TM89ENdPSGWDAUo05JMh4HsOc+/cu9MUmKaQVwEoSxAKSzHb2MlfytIzgiB2XZCsCy'
+ b'cQiA0wCAAQBFDPJiispTvkJZg1WAVTAEaGMwXPto4/YxBWB5ad6SfIfyCroBzAbCUY01dwWp'
+ b'SxJxzb/GAnTIg9+vAbEfog+uEKWPv9DafeZI9cdWAE4tRgAcPAvAYk5GfiHrQwCoJJ4Wk7QF'
+ b'QP+nk0294drHt+4cUwBOLMqW8pxqvU+RKnDAccFmV1cfWGhG1cStbgkqHgkAcAKFIP0D13nu'
+ b'n9p6Hhmp/tgKwMlF2RwANI8YwPhzCwQA6AIsnBmk68L/NfSFa1dtaxnzSuDJxdk3FDkdt2H0'
+ b'jws2QQgG2/vCYAVoPBgc8HUYg4/SHwAyPUzJxJc6gltGqi+2AnBiYVbNeADAhVGzjgDks97W'
+ b'VggCJUsAaDwG0EljKFr7dPPuMQdgcV5GcYXH2QBgO9BCAQRsR2eQABBUGWAFBlqDoSob/X8E'
+ b'D4xgZP1z7cGpI9kXWwE4riBTAIC3juPjYnJzAYC2/iwg2RjAyAIQgGd3tI05ACjHFmQ/M84l'
+ b'n4Qw90Fu3hsKs94oBINUBIN0hGY/CrrAELjAXkrvfaU9WDOS/bAVgGMBgBKXI54F+LJzWRAA'
+ b'cFBqOQiMAARbwtHaF3a1pwQAJxRlH50pS391QPqHcMbgy+5giK/6yUaZe7gzX/TeKABBGg2x'
+ b'1PHPtvc8P5L9sBWAo/MzasbFAWDEm5nNghADOIwYINliCDeB8PtbI7Hav7Z0pAQAR+RlyFmK'
+ b'ssEj0wlY08L2dQXDWLcTlUHj/42EgtF9QvoXbtX1wg+C4c6R7IetAHw7DwBwqgAAjQPQwwEQ'
+ b'myOSBQCXQdGFbItotX9r7UwJAFCOzMv8UY4q34IuDQcpGo2ynnCU1wSslLq/uv988HEV8O1X'
+ b'ukMHj3QfbAVgSa6/phgBAAVBAEO8GZkAQLsRBFoHoDkaq32lrTtlADgw2z++xCFvUmRJ5RU7'
+ b'CAbbgiEIBgkPBkdCvaIAxEgvYz95Kxj+6Uj3wVYAFuf4aooAAIeRBnr9GaynvYMrR0l+OwAH'
+ b'AH9/e1SrfbU9dQBAWZTjfyFPVZZi4IfrA7FIjIU1jTpJcqXufQn2O8z3AZCD3wyG3x7p9tsK'
+ b'wKHZAgA0kRjJe30AQBsCQIxScHLC00B43RHTal/v6EkpAJbkZvyHV6LPo1XTxClmrDMUoWj1'
+ b'5GFCwMTAYwDcoRFS8H/B8Ig/PtdWAA7J8tYUAgCKoRwPPkGrQ1gAmVhYCyBCETsBgDc7gykF'
+ b'wMHZPsUnSZsh8BuPiQ3C2heJ4O4NXvAabgaAwWUfI8+/2xs+3o722wrAQQiAQxEA6AiAl/V0'
+ b'dIo6QJKzgy+E8CIIAwD02re7elMKANFP308hJbwZBzzGj7PVWB+vCVBrzwseJHzjCVoBQmre'
+ b'7Y3ca0fbbQXgwExPTT4AoFJhAVweAKBTAKCQfd8ajsLvCyQiFWrR9Np3u/tSDoDZmd7yfFna'
+ b'qBiPBsVgsDscISp00codwYMFY58QRIHgAqatDkXX29F2WwGYnwEAqDK3ABjNOt0e1tvVxRdN'
+ b'5KTPCGTxIHAXAPB+TyjlAEA5INP7v5kyPQornGgFWFRjEU3nRa8hxQGY+YgYYOu7oUiZXe22'
+ b'FYB5Ge6afAVdgAgCTQB4EJhkKVhsiuR1cG4BVgfDKQnAwkzviS5K/ox95UfEgs8LRmIcgGSL'
+ b'XomiE1H86iXk4bpQ9Dy72m0rAHP97po8tADUsAAuN+vt7jLSQGt7AjEIbAUAPuxNTQDm+N2q'
+ b'W6KNEAwWY88w5omEY4wSXCZObul7QJ95AQgnCjnj3VD0CbvabSsAs32umtxEF+B0s77ubiMI'
+ b'tLAfgAgXgACs6Y2kJAAoczM8t7kpu0GhovJJgYJQTBOlbwuqZkbQG0HvR2nJmlB0p11tthWA'
+ b'WV5nTY6a4AIAgFB3DwBALK0FiNVAsAC6XlvXl7oATPd7Kv2ErYf+yegFKESw4AaIA3fxW3AD'
+ b'PP3jf4B8/EEoNsvONtsKwH4AQLYirzT9ogMAiPQEjUKQhVIw3xFESBsA8HEKA4Ayw+t6xSeR'
+ b'JRgMYuYiaRoDawCxQPJuQDfMf4eu/3JzTLf17CNbAZjhQQCklVj1w3ROdbpYtKeXuwArj9E1'
+ b's4A2jdV+EkptAAJe16kqZasUPD8ZT/sH8rkbQOhJEtRj/AgIhOHVLUlH/jMUfdnO9toKwHS3'
+ b'oyZLBQAo5bc1OZxOFg32CRdArNUB0AW06ax2bYoDMNXjdDopaVQJKUT18lNC8O4R+EFNMvCN'
+ b'8h1ABDca5q+L6raef2grAFPdak0mugAqXABaAK23T7gAYi0IxCygHQD4LMUBQKl2O34BKeF1'
+ b'PBjEh0fqOouiGyBJQI+1f9HnV9dGNNtPQLUVgCoXAiANBKAPLAAZggsApbSDC1gXjqY8AJPc'
+ b'jiovIetkkezwYDAc1Xk6qPQfo7pHEZtfAHjCbqiP6XfY3VZbAZjiUmoyZNlwARAEOpxMD4WM'
+ b'QlDyFkAzloTRAnyeBgCgTHY733AR/VDJSAllsAKQFfL456t6rYnVPzQE+38R0z+0u522AlDp'
+ b'RABEEJgIAC8FJ/3xRgzAeFRcuz4cSwsAqtyOM8D0P8atH/wsM52FIRRw4pkBX2EB+M4iQnbD'
+ b'/ygGAGx/DoKtAFQAAH4pAQDVyVgELAAZwloAvHaCBfgiTQCY6FRdKmFNAEAuP1Qb7+8HE4B5'
+ b'wN6qoGb+H2LsqUaNnTYa7bQVgIkOAECm/VmA6sD6KFcArwQmvRws0kAEoD6SHgCgjHOq9/gI'
+ b'uxL7j8UsBSjAmgBkCHu0AppY+sVBuQj8f+1otNFWAModsrAACTEACQsArO4JROV0Mlb7ZRoB'
+ b'UOpyzHDq2sfQfwlBlgHniCbWBvYUBAv/j2dEsooGjW0ejTbaCkCZKtf4MAagJG4BaCQSTwOT'
+ b'JcCMAboAgA1pBADXgVN9y8H0g3ADDFoxFayAhg8TG7wnkolUF/q6YZOmTx6t9tkKQCkA4JVo'
+ b'vwVAAKJRiptBZAubQvnZeEQAsDHNACh2KGe7GHvEnAQK9CiqE6Mm0K8DfjQs4QD8V6OmXzJa'
+ b'7bMVgPGKBAD0B4Gq4mBSNGLZBehGENilk9pN0fQCoESVPWDtmuDKpqI/jOIaAS4QkX4d8F1P'
+ b'hANy8gaN/Xm02mcrACUAgEdKdAHqAACsZAGa4QI2R7W0AgClwKn+xqVpl5vBoIqbhw03YJ6W'
+ b'iBYOXEAsQvTCHToZtaei2wrAOA5AvwtQAQCZuwBq/XwAtACM1DakIQBFDiWg6PpHMhYFCY9/'
+ b'eFEoHgsR4f/BUP5zk8YWjGbbbAWgGABwUyosAPzsUAwAjC1hyVsAUQzqBhfQEEs/AFCKVPk9'
+ b'SAPn863jqAsdD8InXBcoUbHieVszIz8czXbZCkCRQjkAmPNiMKcCAErMAMDCfQGauDee9IAF'
+ b'aLQOQLJH99kqeap8Pgz6g6Y7VEEjMcMNoER4mkgWbdHZP0azXbYCUCjTGhc1XADo3cEBiHEA'
+ b'kt0SZpaCteQA2JtZYYNeR10KFMknMR4MZprvSXiujNFe6GM3WLqCHYyELP5pfiqt8coGXUn9'
+ b'sn2dTgSAGQBosQTfZ2VPICFBndVu0fQ4AJBO0lh/5xMHP1EJerLKsFuyFem/ICW8SDKCQTxe'
+ b'RDPaDJ3461adHZvEnzEHnGfTxntawmWpr7YCkB8HwMgCFAUA0OJ1gH0+MZQYN4cSMYoAwIO9'
+ b'jF3SAwEUbpiN4RpL/8CbAx4j/b+S9EwYDclT5P1lpr+Pz1ZHi6hAF2OGEmCCXLOLkV/t5Vdx'
+ b'wHGwnXA5jJ+xjxHjig21n/YCINEaJ2YBRIyGCgA4AACZmJXAQR9vPiWUDJy+4sRsvkXmsSaN'
+ b'75FLNOlmCm1eGkmxgU+UPFlaLTM2F28Y0ngsQBj4fxolbGYHI2vN/wcjLOlisN1weYj4HgVd'
+ b'RK/xGjXUM2SxFYBcAMCVCICsMFU3LAD/dGo8GJrs4Yofkqz3EdIUZOydbp29CK7gS/jndrhw'
+ b'qxQelhwm/QM/Iocn2inZinKxomv3mToBXWAwuD1MWBm/ExgGGjriYSJWwAtnPc7wbrg64Ooy'
+ b'+pz6B0XmUAGAJMaZWwCVxwA0XgDpn+n9p2LjWzCyfWDqW2DQvwSFfEHEVQ9XA1y74fd79YEz'
+ b'Pi3EQ2mGh5Im+NZv1gQoY6taGKmB/uTBWwVwZRMx8D1wbYdrBxGDHyIjDLmtAGQCAKIQZMYA'
+ b'Mo8BzA2hfI2c9A96iJFwN2M9MPC7YVRb4L1tcG0gYvDxtQn+VDvMmlBUDHxail9VvgMvE8Hv'
+ b'e0AH2RTPl2IMrRrOeLRqCMiXxivO+CixCXLbAPBSflraFV6J/gpzfpzhwgJoiZ4fBz0Cgx6E'
+ b'AK8d7FwrvId3wWyFCw9DbDBe8b12AKeX4kO108DUDxZjQ7jKhHkvgu9xxQ+fBzieiMFthOsz'
+ b'uDbBhU9ART9vO+S2AKAK7477Hq7MluidhqsnTkXEAGEY9B4YdJjpnTCYbUaHcbZvMRSBAOCg'
+ b'o9kLwq+HqCiW6SyNzL0haPAwkMuBq5SIQZ9IhIlH4NGy4WzfBn3sxn7qo9hHWwAA2lXoRCb0'
+ b'/DgnJacTQXy2LEmOPl3vhaAHBxafdjV40NHXoSlE3xcfdJJCuXySwmc7XH4ifHo5XMVweYkw'
+ b'8c1EmPdd8B+74MIbgcbEqo00AGaBwm90Go81nUIE+ejfcBAxmsXAZotx4aC3Ge+bqU1iHp9O'
+ b'gv3HmZ1BRECXb+iCb2cw+tlmfG8s/4+tjBQAaOZw4DFX9RgKyCXC7PmIMIH4WejXcPa3GorA'
+ b'mY4zIkr6ixlp599J/8DjDM80XhWjX9hHhBvT1ghJMbCHCoD5e4kD7zCUYKb5ZnWOkIEFG1TC'
+ b'4Nw9ZQs3+xAceDT1LiIgN4s12De0ZibcYz7T9yZDAUAi/aVJhfQPOL5nzmCzHDu4OLOnmk86'
+ b'iunjHcarZPQT4TatWVpYsmQBSFxskRKuxMUXfdCVroO7Lz2YwMuk37INaSEmFSQZAAavtA3a'
+ b'y5r2szkZSQTf7LcJeloLHeK/mUr4d5DBwH+txNZS8DeS+vINAP/m8g0A/+by//Kr6Y9Iz7ne'
+ b'AAAAAElFTkSuQmCC')
+
+
+SettingsIcon = PyEmbeddedImage(
+ b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB0UlEQVR4Xo2RT0gbQRTG30Yb'
+ b'w5o2Em20pB4MtlgItkUo9VRQs0eDJ4UepEUQWkquPfQgqCUlAfHWSwOiDUQLLb0UZgUV2+hB'
+ b'UUhy6EWlf0gx0S60G9vGzXNmkhEsLtkHy/veN/v7eLMrQZVqebYYaGhqIGLW8pryY6JPFbPN'
+ b'Chy60wyxgWvAugirFsBhl/sSedTlgY5mJ2x80WBqLcs3ACvVHlnHyPIefvh8iFHa2cxCRXh7'
+ b'VMwm1Rb+hAvpHE4s7iDVZ2BfOInjKvVpN72CcWyA/s+Ao2KJ6hII2O6oI0M3PdDaKEPJMMD0'
+ b'L3jHlvCpch3YGzt5HeY3v3N/sMsLbY31sHugQ4J65wY4Q+8DzosyedLLA3h9PShw3eqWuX63'
+ b'9Y3ZinQe7HDYidLppWvWA6cAaZPKQblfQNJZ+HP0V/k93a9K/8MXamvIPb8XrrgZDJD9qcNK'
+ b'qrw+VuKKxwaHoTKfwnQg3f6rcNkl84N9rQDJDF9VACY1/DogP36L3dPrGJzLYHA2wzXz2JkA'
+ b'TGH76Bu8FU1iTyyFPa9SeJtq5nG4WtWOJPDGi4949+UWfba5pp41WGxgexBH3/NV9E2uou1h'
+ b'3DosPmIFIBVPgZn7qtWAE7Xe3IneUIDPAAAAAElFTkSuQmCC')
diff --git a/Quick_IP_Converter.py b/Quick_IP_Converter.py
new file mode 100644
index 0000000..bdf7c92
--- /dev/null
+++ b/Quick_IP_Converter.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+import GUI
+import libIPconv as conv
+import wx
+
+
+class MainFrame(GUI.IPConverterFrame):
+ def __init__(self, *args, **kwds):
+ GUI.IPConverterFrame.__init__(self, *args, **kwds)
+ self.last_changed = self.text_ctrl_hex
+ self.text_ctrl_dec.addr_type = conv.ADDRTYPE.DEC
+ self.text_ctrl_dotted.addr_type = conv.ADDRTYPE.DOTTED
+ self.text_ctrl_hex.addr_type = conv.ADDRTYPE.HEX
+
+ main_converter.register_callback(self.text_ctrl_dec.ChangeValue, conv.ADDRTYPE.DEC)
+ main_converter.register_callback(self.text_ctrl_dotted.ChangeValue, conv.ADDRTYPE.DOTTED)
+ main_converter.register_callback(self.text_ctrl_hex.ChangeValue, conv.ADDRTYPE.HEX)
+
+ main_converter.reverse = self.checkbox_reverse.IsChecked()
+
+ def monitor_clipboard(self):
+ success, clipboard_content = super().monitor_clipboard()
+ if success and clipboard_content:
+ trimmed = clipboard_content.strip()
+ if conv.RECLIST[conv.ADDRTYPE.HEX].fullmatch(trimmed) and (trimmed != self.text_ctrl_hex.GetValue()):
+ self.text_ctrl_hex.SetValue(trimmed)
+ elif conv.DOTTEDQUADIP_STRICTREC.fullmatch(trimmed) and (trimmed != self.text_ctrl_dotted.GetValue()):
+ self.text_ctrl_dotted.SetValue(trimmed)
+
+ def on_checkbox_reverse(self, event):
+ main_converter.reverse = event.IsChecked()
+ main_converter.set_value(self.last_selected.GetValue(), self.last_selected.addr_type)
+
+ def on_char(self, event):
+ super().on_char(event)
+
+ if event.GetSkipped():
+ return # return if the key was already allowed
+
+ event_control = event.GetEventObject()
+ event_key = event.GetKeyCode()
+
+ if conv.filters.isAllowedASCII(event_key, event_control.addr_type):
+ event.Skip()
+ if event_control.addr_type == conv.ADDRTYPE.DOTTED:
+ control_content = event_control.GetValue()
+ control_selection = event_control.GetSelection()
+
+ # Insert the new character at the insertion point, over-writing any selected characters
+ first = control_content[0:control_selection[0]]
+ last = control_content[control_selection[1]:]
+ check_value = first + chr(event_key) + last
+
+ if event.GetSkipped():
+ if '.' not in check_value:
+ check_value += '.'
+ if check_value.endswith('.'):
+ check_value += '0'
+ if not conv.isValidIPv4(check_value, conv.ADDRTYPE.DOTTED):
+ event.Skip(False)
+ else:
+ event.Skip()
+
+ def on_paste(self, event):
+ success, pasted_string = super().on_paste(event)
+
+ event_object = event.GetEventObject()
+
+ filtered_string = conv.filters.filterChars(pasted_string, event_object.addr_type)
+ if not filtered_string:
+ return # nothing to insert
+
+ control_content = event_object.GetValue()
+ control_selection = event_object.GetSelection()
+
+ # Insert the new string at the insertion point, over-writing any selected characters
+ first = control_content[0:control_selection[0]]
+ last = control_content[control_selection[1]:]
+ final_value = first + filtered_string + last
+
+ event_object.SetValue(final_value)
+ event_object.SetInsertionPoint(len(first + filtered_string))
+
+ def on_text(self, event):
+ event_object = event.GetEventObject()
+ main_converter.set_value(event_object.GetValue(), event_object.addr_type)
+ self.last_changed = event_object
+
+
+class MainApp(wx.App):
+ def OnInit(self):
+ self.frame_main = MainFrame(None, wx.ID_ANY, "", name='MainFrame')
+ self.SetTopWindow(self.frame_main)
+ self.frame_main.Show()
+ return True
+
+
+if __name__ == "__main__":
+ main_converter = conv.Converter()
+ app = MainApp(0)
+ app.MainLoop()
diff --git a/README.md b/README.md
index 01120bc..e9749c0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,92 @@
-# Quick-IP-Converter
+# Quick IP Converter
+
+Copyright (C) 2014, 2018 Brandon M. Pace
+
+License: LGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this program.
+ If not, see .
+
Convert values between decimal, hexadecimal and dotted-quad IP formats
+
+Dotted Quad accepts IPv4 addresses or subnet masks in the following formats:
+
+ - 192.168.10.1
+ - 255.255.240.0
+
+Hex accepts hex values with optional 0x or 0X prefix:
+
+ - 0xc0a80101
+ - 0X0A0A0A01
+ - 1bb
+
+Decimal will accept any valid decimal value with no separators:
+
+ - 123
+ - 23456
+
+**Notes:**
+
+ - The clipboard monitoring action only runs when the application does not have focus.
+ - 2.0 release removed most hotkeys
+ - Reverse checkbox triggers conversion from the last selected text box
+
+
+**Revision history:**
+
+ 2.0: Completely new GUI that is smaller in size
+ - Removed IP checkbox
+ - Removed About button
+ - Added option to monitor clipboard for hex or dotted quad values once a second and automatically run conversion
+ - The clipboard is only monitored when another application has focus.
+ - Allow for much larger numbers for decimal-hex conversion
+ - All hotkeys removed except for Ctrl-Win-Z, which will take a hex value from the clipboard and run conversion
+ - Added a settings window, which contains the About link
+ - Added option for dark theme
+ - Added option to disable 'stay on top'
+ - Reverse option is now true byte-order flip for all conversions
+ - Reverse checkbox now triggers conversion from the last selected TextCtrl
+ - Paste now works properly instead of replacing the entire contents of the TextCtrl
+ - Window position and settings are saved when the exit button in the window is clicked
+
+ 1.8: Updated to use Python 3.6.5 and wxPython Phoenix 4.0.3
+ - Pressing Enter is no longer necessary to trigger calculation. You can type or paste values and calculation will automatically run.
+ - Clicking inside of a text box now selects all text if it wasn't already in focus.
+ - The text is not copied to the clipboard in case you are trying to paste current clipboard data in the same place.
+ - Each text box now only allows relevant characters to be placed there.
+ - Limitation: The largest decimal number supported is 18446744073709551615 (8 bytes, hex ffffffffffffffff)
+
+ 1.6: Further enhancements to facilitate keyboard-only use. (even with the window not in focus, so these are Global)
+ - Added keyboard shortcuts that process data from the Windows clipboard and toggle the IP/Reverse checkboxes as needed: (Thanks goes to Dan Cross for the recommendation)
+ - Win + Z = Process Hex IP (enables IP)
+ - Win + X = Process Dotted-Quad IP (enables IP)
+ - Ctrl + Win + Z = Process Hex to Decimal
+ - Ctrl + Win + X = Process Decimal to Hex
+ - Win + A = Toggle Reverse
+
+ If IP is unchecked and Reverse gets checked, both will get selected.
+ If IP and Reverse are both checked, and IP gets unchecked, both will get deselected.
+
+
+ 1.4: Focus was on user experience and allowing keyboard-only use:
+ - Removed buttons. Input is now processed upon pressing 'Enter'
+ - Added conversion to Decimal IP.
+ - Background color normalized
+ - Using the 'Tab' key to traverse the window is now enabled.
+
+ 1.2:
+ - Added 'IP' checkbox to allow for non-IP conversion between decimal-hex.
+
+ 1.0:
+ - Initial release with decimal-hex conversion and reverse option.
diff --git a/libIPconv/__init__.py b/libIPconv/__init__.py
new file mode 100644
index 0000000..46db00a
--- /dev/null
+++ b/libIPconv/__init__.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+from . import filters
+from .converter import *
diff --git a/libIPconv/conversions.py b/libIPconv/conversions.py
new file mode 100644
index 0000000..b44b1c3
--- /dev/null
+++ b/libIPconv/conversions.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+from .convregex import *
+from .globals import *
+
+
+def convertStrToType(input_val: str, input_type: int, output_type: int, reverse: bool = False, safe: bool = False) -> str:
+ """
+ Be sure to use isValidIPv4() before this to sanity-check your input!
+ :param input_val: str value to convert
+ :param input_type: int value type from ADDRTYPE enum determining what the source type is
+ :param output_type: int value type from the ADDRTYPE enum determining what the destination type is
+ :param reverse: bool for whether or not to reverse the byte-order
+ :return: str converted value or '' on error/failure
+ """
+ return_value = ''
+ try:
+ if input_type == output_type:
+ raise ValueError('output_type should be different than input_type')
+
+ if input_type == ADDRTYPE.DEC:
+ if output_type == ADDRTYPE.DOTTED:
+ return_value = decStrToDottedQuadStr(input_val, reverse)
+ elif output_type == ADDRTYPE.HEX:
+ return_value = decStrToHexStr(input_val, reverse)
+ elif input_type == ADDRTYPE.DOTTED:
+ if output_type == ADDRTYPE.DEC:
+ return_value = dottedQuadStrToDecStr(input_val, reverse)
+ elif output_type == ADDRTYPE.HEX:
+ return_value = dottedQuadStrToHexStr(input_val, reverse)
+ elif input_type == ADDRTYPE.HEX:
+ if output_type == ADDRTYPE.DEC:
+ return_value = hexStrToDecStr(input_val, reverse)
+ elif output_type == ADDRTYPE.DOTTED:
+ int_value = hexStrToDecStr(input_val, False)
+ return_value = decStrToDottedQuadStr(int_value, reverse)
+ except ValueError:
+ if not safe:
+ raise
+ else:
+ return ''
+ else:
+ return return_value
+
+
+def decStrToDottedQuadStr(input_value: str, reverse: bool = False) -> str:
+ if RECLIST[ADDRTYPE.DEC].fullmatch(input_value):
+ return decToDottedQuadStr(int(input_value), reverse)
+ else:
+ return ''
+
+
+def decToDottedQuadList(input_value: int, reverse: bool = False) -> list:
+ if (input_value < 0) or (input_value > V4MAXVAL):
+ raise ValueError(f'Input value is outside of IPv4 range: {input_value}')
+ if reverse:
+ byte_order = 'little'
+ else:
+ byte_order = 'big'
+ return [octet for octet in input_value.to_bytes(4, byte_order)]
+
+
+def decStrToHexStr(input_value: str, reverse: bool = False) -> str:
+ return_value = ''
+ if input_value:
+ if reverse:
+ byte_order = 'little'
+ else:
+ byte_order = 'big'
+ int_value = int(input_value)
+ return_value = int_value.to_bytes(((int_value.bit_length() + 7) // 8), byte_order).hex()
+ return return_value
+
+
+def decToDottedQuadStr(input_value: int, reverse: bool = False) -> str:
+ return '.'.join([str(octet) for octet in decToDottedQuadList(input_value, reverse)])
+
+
+def dottedQuadStrToDecStr(input_value: str, reverse: bool = False) -> str:
+ split_value = [int(value) for value in input_value.split('.')]
+ if reverse:
+ byte_order = 'little'
+ else:
+ byte_order = 'big'
+ return str(int.from_bytes(split_value, byte_order))
+
+
+def dottedQuadStrToHexStr(input_value: str, reverse: bool = False) -> str:
+ split_value = [int(value) for value in input_value.split('.')]
+ if reverse:
+ split_value.reverse()
+ byte_value = bytes(split_value)
+ return byte_value.hex()
+
+
+def hexStrToDec(input_value: str, reverse: bool = False) -> int:
+ if RECLIST[ADDRTYPE.HEX].fullmatch(input_value):
+ trimmed = input_value.lstrip('0xX')
+ if len(trimmed) % 2:
+ trimmed = '0' + trimmed
+ byte_value = bytes.fromhex(trimmed)
+ if reverse:
+ byte_order = 'little'
+ else:
+ byte_order = 'big'
+ return int.from_bytes(byte_value, byte_order)
+ else:
+ return -1
+
+
+def hexStrToDecStr(hex_addr: str, reverse: bool = False) -> str:
+ check_value = hexStrToDec(hex_addr, reverse)
+ return '' if (check_value == -1) else str(check_value)
+
+
+def isValidIPv4(input_value, addr_type: int = ADDRTYPE.NONE, strict: bool = False) -> bool:
+ """
+ Accepts string or int value and returns True if it is a value in the range of valid IPv4 addresses.
+ The addr_type argument should be a value from the ADDRTYPE enum. If input is int you must use NONE or DEC addr_type.
+ The string can be a dotted-quad format, hex format, or decimal format. (no leading or trailing whitespace)
+ strict mode will require dotted-quad format to include four valid octets
+ """
+ check_value = -1
+
+ if isinstance(input_value, str):
+ if '.' in input_value and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.DOTTED]):
+ # Confirm match for dotted-quad format
+ if len(input_value) <= 15 and IP_RECLIST[ADDRTYPE.DOTTED].fullmatch(input_value):
+ if strict and not DOTTEDQUADIP_STRICTREC.fullmatch(input_value):
+ check_value = -2
+ else:
+ check_value = 0
+ else:
+ check_value = -3
+ elif IP_RECLIST[ADDRTYPE.DEC].fullmatch(input_value) and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.DEC]):
+ check_value = int(input_value)
+ elif IP_RECLIST[ADDRTYPE.HEX].fullmatch(input_value) and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.HEX]):
+ check_value = int(input_value, 16)
+ elif isinstance(input_value, int):
+ if addr_type in [ADDRTYPE.NONE, ADDRTYPE.DEC]:
+ check_value = input_value
+ else:
+ raise ValueError('Type int input_value passed with incompatible addr_type of {addr_type}')
+ else:
+ raise ValueError(f'Expected input type str or int. Got {type(input_value)}')
+
+ return (check_value >= 0) and (check_value <= V4MAXVAL)
diff --git a/libIPconv/converter.py b/libIPconv/converter.py
new file mode 100644
index 0000000..0c6d436
--- /dev/null
+++ b/libIPconv/converter.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+from .conversions import *
+
+
+class Converter(object):
+ _supported_addr_types = [ADDRTYPE.DEC, ADDRTYPE.DOTTED, ADDRTYPE.HEX]
+
+ def __init__(self, safe=True):
+ self.reverse = False
+ self.safe = safe
+ self._callbacks = {key: None for key in self._supported_addr_types}
+ self._values = {key: '' for key in self._supported_addr_types}
+
+ def is_addr_type_supported(self, addr_type: int) -> bool:
+ return addr_type in self._supported_addr_types
+
+ def register_callback(self, callback_function, addr_type: int):
+ """
+ Allows calling a single-argument function for values changed as a result of set_value.
+ When one value changes, callbacks for the others are called with the converted string value,
+ which can be '' if the set value is invalid.
+ :param callback_function: function that takes a single argument (the new value)
+ :param addr_type: int from the ADDRTYPE enum
+ :return: None
+ """
+ if not callable(callback_function):
+ raise ValueError('callback_function is not callable')
+ self._check_addr_type(addr_type)
+
+ self._callbacks[addr_type] = callback_function
+
+ def reset_values(self):
+ for key in self._values.keys():
+ self._values[key] = ''
+
+ def run_callbacks(self, addr_type: int):
+ """Run callback functions for types other than the input addr_type"""
+ for typeval in self._supported_addr_types:
+ if (typeval != addr_type) and self._callbacks[typeval]:
+ self._callbacks[typeval](self._values[typeval])
+
+ def run_conversions(self, addr_type: int):
+ """Convert the currently stored value of addr_type to other types"""
+ self._check_addr_type(addr_type)
+ for typeval in self._supported_addr_types:
+ if typeval != addr_type:
+ self._values[typeval] = convertStrToType(
+ self._values[addr_type], addr_type, typeval, reverse=self.reverse, safe=self.safe)
+
+ def set_value(self, value: str, addr_type: int):
+ self._check_addr_type(addr_type)
+
+ self._values[addr_type] = value
+
+ if not value:
+ self.reset_values()
+ else:
+ self.run_conversions(addr_type)
+
+ # call callbacks
+ self.run_callbacks(addr_type)
+
+ def _check_addr_type(self, addr_type: int):
+ if not self.is_addr_type_supported(addr_type):
+ raise ValueError(f'addr_type value of {addr_type} is not supported')
diff --git a/libIPconv/convregex.py b/libIPconv/convregex.py
new file mode 100644
index 0000000..f8d1aa1
--- /dev/null
+++ b/libIPconv/convregex.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+import re
+
+
+# regex for matching decimal, dotted-quad and hex IP - including compiled versions for performance
+DECIP_RE = r'^([0-9]{1,10})$'
+DECIP_REC = re.compile(DECIP_RE)
+
+HEXIP_RE = r'^((0[xX])?[0-9a-fA-F]{1,8})$'
+HEXIP_REC = re.compile(HEXIP_RE)
+
+# Requires 0-255 before and after each '.', up to 3 instances of '.'
+DOTTEDQUADIP_RE = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){0,3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
+DOTTEDQUADIP_REC = re.compile(DOTTEDQUADIP_RE)
+
+IP_RELIST = [DECIP_RE, HEXIP_RE, DOTTEDQUADIP_RE]
+IP_RECLIST = [DECIP_REC, HEXIP_REC, DOTTEDQUADIP_REC]
+
+# Requires 0-255 before and after each '.' with 3 instances of '.'
+DOTTEDQUADIP_STRICTRE = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
+DOTTEDQUADIP_STRICTREC = re.compile(DOTTEDQUADIP_STRICTRE)
+
+# regex for matching general decimal and hex - including compiled versions for performance
+DEC_RE = r'^([0-9]+)$'
+DEC_REC = re.compile(DEC_RE)
+
+HEX_RE = r'^((0[xX])?[0-9a-fA-F]+)$'
+HEX_REC = re.compile(HEX_RE)
+
+RELIST = [DEC_RE, HEX_RE]
+RECLIST = [DEC_REC, HEX_REC]
diff --git a/libIPconv/filters.py b/libIPconv/filters.py
new file mode 100644
index 0000000..17b907d
--- /dev/null
+++ b/libIPconv/filters.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+import string
+from .globals import ADDRTYPE
+
+
+dec_allowed_ascii = [ord(ch) for ch in string.digits]
+dotted_allowed_ascii = dec_allowed_ascii + [ord('.')]
+hex_allowed_ascii = [ord(ch) for ch in string.hexdigits]
+
+
+def filterASCII(key_codes: list, addr_type: int) -> list:
+ if addr_type == ADDRTYPE.DEC:
+ retval = [key for key in key_codes if key in dec_allowed_ascii]
+ elif addr_type == ADDRTYPE.DOTTED:
+ retval = [key for key in key_codes if key in dotted_allowed_ascii]
+ elif addr_type == ADDRTYPE.HEX:
+ retval = [key for key in key_codes if key in hex_allowed_ascii]
+ else:
+ raise ValueError(f'addr_type of {addr_type} is not valid')
+ return retval
+
+
+def filterChars(chars: str, addr_type: int) -> str:
+ if addr_type == ADDRTYPE.DEC:
+ retval = ''.join(ch for ch in chars if ch in string.digits)
+ elif addr_type == ADDRTYPE.DOTTED:
+ retval = ''.join(ch for ch in chars if ch in string.digits or ch in '.')
+ elif addr_type == ADDRTYPE.HEX:
+ retval = ''.join(ch for ch in chars if ch in string.hexdigits)
+ else:
+ raise ValueError(f'addr_type of {addr_type} is not valid')
+ return retval
+
+
+def isAllowedASCII(key_code: int, addr_type: int) -> bool:
+ if addr_type == ADDRTYPE.DEC:
+ retval = key_code in dec_allowed_ascii
+ elif addr_type == ADDRTYPE.DOTTED:
+ retval = key_code in dotted_allowed_ascii
+ elif addr_type == ADDRTYPE.HEX:
+ retval = key_code in hex_allowed_ascii
+ else:
+ raise ValueError(f'addr_type of {addr_type} is not valid')
+ return retval
+
+
+def isAllowedChar(char: str, addr_type: int) -> bool:
+ if (len(char) != 1):
+ raise ValueError(f'char input includes more than one character: {char}')
+ if addr_type == ADDRTYPE.DEC:
+ retval = char in string.digits
+ elif addr_type == ADDRTYPE.DOTTED:
+ retval = char in string.digits or char in '.'
+ elif addr_type == ADDRTYPE.HEX:
+ retval = char in string.hexdigits
+ else:
+ raise ValueError(f'addr_type of {addr_type} is not valid')
+ return retval
diff --git a/libIPconv/globals.py b/libIPconv/globals.py
new file mode 100644
index 0000000..0923322
--- /dev/null
+++ b/libIPconv/globals.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+# Copyright (C) 2014, 2018 Brandon M. Pace
+#
+# This file is part of Quick IP Converter
+#
+# Quick IP Converter is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Quick IP Converter is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Quick IP Converter.
+# If not, see .
+
+
+import enum
+
+
+# list of bit-masks for each octet of an IPv4 address
+V4MASKS = [255 << (8*octet_offset) for octet_offset in range(4)]
+
+
+V4MAXVAL = int('0xffffffff', 16) # 4294967295 or 255.255.255.255
+
+
+# enum values that can be used for index of the IP_RE*LIST items
+@enum.unique
+class ADDRTYPE(enum.IntEnum):
+ NONE = -1
+ DEC = 0
+ HEX = 1
+ DOTTED = 2
diff --git a/pyinstaller_build.bat b/pyinstaller_build.bat
new file mode 100644
index 0000000..50a103d
--- /dev/null
+++ b/pyinstaller_build.bat
@@ -0,0 +1 @@
+python.exe -OO -m PyInstaller -w --version-file pyinstaller_version_file.txt --icon=GUI\raw_resources\IPconv.ico Quick_IP_Converter.py
diff --git a/pyinstaller_version_file.txt b/pyinstaller_version_file.txt
new file mode 100644
index 0000000..3235f95
--- /dev/null
+++ b/pyinstaller_version_file.txt
@@ -0,0 +1,55 @@
+# UTF-8
+#
+# For more details about fixed file info 'ffi' see:
+# http://msdn.microsoft.com/en-us/library/ms646997.aspx
+# HOW TO UPDATE:
+# Change the filevers variable
+# Change the ProductVersion in the StringTable
+# To use this file when running PyInstaller: python.exe -OO -m PyInstaller -w --version-file pyinstaller_version_file.txt --icon=GUI\raw_resources\IPconv.ico Quick_IP_Converter.py
+# (-OO is optional and is for additional bytecode optimization)
+VSVersionInfo(
+ ffi=FixedFileInfo(
+ # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
+ # Set not needed items to zero 0.
+ # NOTE: change filevers ONLY. To change product version, edit ProductVersion in the StringTable!
+ # LEAVE the 4-part versioning, just use 0 where you don't have any version info. Ex: 4.4 will be 4, 4, 0, 0
+ filevers=(2, 0, 0, 0),
+ prodvers=(0, 0, 0, 0),
+ # Contains a bitmask that specifies the valid bits 'flags'r
+ mask=0x3f,
+ # Contains a bitmask that specifies the Boolean attributes of the file.
+ flags=0x0,
+ # The operating system for which this file was designed.
+ # 0x4 - NT and there is no need to change it.
+ OS=0x4,
+ # The general type of file.
+ # 0x1 - the file is an application.
+ fileType=0x1,
+ # The function of the file.
+ # 0x0 - the function is not defined for this fileType
+ subtype=0x0,
+ # Creation date and time stamp.
+ date=(0, 0)
+ ),
+ kids=[
+ VarFileInfo([VarStruct(u'Translation', [0, 1200])]),
+ StringFileInfo(
+ [
+ StringTable(
+ u'000004b0',
+ [StringStruct(u'Comments', u''),
+ StringStruct(u'CompanyName', u''),
+ StringStruct(u'FileDescription', u'IPv4 value conversion program'),
+ # NOTE: This 'FileVersion' is over-written with the filevers contents! Change that instead!
+ StringStruct(u'FileVersion', u'0.0.0.0'),
+ StringStruct(u'InternalName', u''),
+ StringStruct(u'LegalCopyright', u'Copyright © Brandon M. Pace 2014, 2018'),
+ StringStruct(u'OriginalFilename', u'Quick_IP_Converter.exe'),
+ StringStruct(u'ProductName', u'Quick IP Converter'),
+ # NOTE: This is where you should change the version:
+ StringStruct(u'ProductVersion', u'2.0.0.0'),
+ # Don't worry about changing the below at all
+ StringStruct(u'Assembly Version', u'0.0.0.0')])
+ ])
+ ]
+)