code:
import wx

from lib.GUI.Common import BetterBitmapButton, BetterButton, BetterChoice, \
	BetterHyperLink, BetterStaticText, ConvertTextToPixelWidth, Gauge, \
	NoneableSpinCtrl, StaticBox, WrapInGrid
from lib.GUI.Dialog import DialogTextEntry, DialogYesNo
from lib.GUI.ListCtrl import BetterListCtrl, BetterListCtrlPanel, SafeNoneInt
from lib.GUI.Menus import AppendMenuItem, AppendMenuCheckItem, AppendSeparator
from lib.GUI.ScrolledPanels import EditPanel, EditSingleCtrlPanel
from lib.GUI.Shortcuts import ConvertKeyEventToSimpleTuple
from lib.GUI.Time import TimeDeltaButton
from lib.GUI.TopLevelWindows import DialogEdit

from lib.Handling.Parsing import MakeParsedTextPretty, StringConverter, StringMatch
from lib.Hydrus.Data import ConvertValueRangeToBytes, TimeDeltaToPrettyTimeDelta, \
	TimeHasPassed, ToHumanBytes, ToHumanInt
from lib.Hydrus.Exceptions import DataMissing, StringConvertException, \
	StringMatchException, VetoException
from lib.Hydrus.Text import DeserialiseNewlinedTexts, StripIOInputLine

from lib.Networking import BandwidthRules

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

from lib.Handling.Parsing import ALPHA, ALPHANUMERIC, NUMERIC, STRING_MATCH_ANY, \
	STRING_MATCH_FIXED, STRING_MATCH_FLEXIBLE, \
	STRING_MATCH_REGEX, STRING_TRANSFORMATION_APPEND_TEXT, \
	STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, \
	STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, STRING_TRANSFORMATION_PREPEND_TEXT, \
	STRING_TRANSFORMATION_DATE_DECODE, STRING_TRANSFORMATION_DECODE, \
	STRING_TRANSFORMATION_ENCODE, STRING_TRANSFORMATION_INTEGER_ADDITION, \
	STRING_TRANSFORMATION_REGEX_SUB, STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, \
	STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, STRING_TRANSFORMATION_REVERSE, \
	transformation_type_str_lookup

### CLASSES ###

class BandwidthRulesCtrl( StaticBox ):
	def __init__( self, parent, bandwidth_rules ):

		StaticBox.__init__( self, parent, 'bandwidth rules' )

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( 'max allowed', 14 ), ( 'every', 16 ) ]

		self._listctrl = BetterListCtrl( listctrl_panel, 'bandwidth_rules', 8, 10, columns, self._ConvertRuleToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		listctrl_panel.SetListCtrl( self._listctrl )

		listctrl_panel.AddButton( 'add', self._Add )
		listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		listctrl_panel.AddDeleteButton()

		#

		self._listctrl.AddDatas( bandwidth_rules.GetRules() )

		self._listctrl.Sort( 0 )

		#

		self.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

	def _Add( self ):
		rule = ( HC.BANDWIDTH_TYPE_DATA, None, 1024 * 1024 * 100 )

		with DialogEdit( self, 'edit rule' ) as dlg:
			panel = self._EditPanel( dlg, rule )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				new_rule = panel.GetValue()

				self._listctrl.AddDatas( ( new_rule, ) )

				self._listctrl.Sort()

	def _ConvertRuleToListCtrlTuples( self, rule ):
		( bandwidth_type, time_delta, max_allowed ) = rule

		pretty_time_delta = TimeDeltaToPrettyTimeDelta( time_delta )

		if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
			pretty_max_allowed = ToHumanBytes( max_allowed )

		elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
			pretty_max_allowed = ToHumanInt( max_allowed ) + ' requests'

		sort_time_delta = SafeNoneInt( time_delta )

		sort_tuple = ( max_allowed, sort_time_delta )
		display_tuple = ( pretty_max_allowed, pretty_time_delta )

		return ( display_tuple, sort_tuple )

	def _Edit( self ):
		selected_rules = self._listctrl.GetData( only_selected = True )

		for rule in selected_rules:
			with DialogEdit( self, 'edit rule' ) as dlg:
				panel = self._EditPanel( dlg, rule )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					edited_rule = panel.GetValue()

					self._listctrl.DeleteDatas( ( rule, ) )

					self._listctrl.AddDatas( ( edited_rule, ) )

				else:
					break

		self._listctrl.Sort()

	def GetValue( self ):
		bandwidth_rules = BandwidthRules()

		for rule in self._listctrl.GetData():
			( bandwidth_type, time_delta, max_allowed ) = rule

			bandwidth_rules.AddRule( bandwidth_type, time_delta, max_allowed )

		return bandwidth_rules

	class _EditPanel( EditPanel ):
		def __init__( self, parent, rule ):

			EditPanel.__init__( self, parent )

			self._bandwidth_type = BetterChoice( self )

			self._bandwidth_type.Append( 'data', HC.BANDWIDTH_TYPE_DATA )
			self._bandwidth_type.Append( 'requests', HC.BANDWIDTH_TYPE_REQUESTS )

			self._bandwidth_type.Bind( wx.EVT_CHOICE, self.EventBandwidth )

			self._max_allowed_bytes = BytesControl( self )
			self._max_allowed_requests = wx.SpinCtrl( self, min = 1, max = 1048576 )

			self._time_delta = TimeDeltaButton( self, min = 1, days = True, hours = True, minutes = True, seconds = True, monthly_allowed = True )

			#

			( bandwidth_type, time_delta, max_allowed ) = rule

			self._bandwidth_type.SelectClientData( bandwidth_type )

			self._time_delta.SetValue( time_delta )

			if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
				self._max_allowed_bytes.SetValue( max_allowed )

			else:
				self._max_allowed_requests.SetValue( max_allowed )

			self._UpdateEnabled()

			#

			hbox = wx.BoxSizer( wx.HORIZONTAL )

			hbox.Add( self._max_allowed_bytes, CC.FLAGS_VCENTER )
			hbox.Add( self._max_allowed_requests, CC.FLAGS_VCENTER )
			hbox.Add( self._bandwidth_type, CC.FLAGS_VCENTER )
			hbox.Add( BetterStaticText( self, ' every ' ), CC.FLAGS_VCENTER )
			hbox.Add( self._time_delta, CC.FLAGS_VCENTER )

			self.SetSizer( hbox )

		def _UpdateEnabled( self ):
			bandwidth_type = self._bandwidth_type.GetChoice()

			if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
				self._max_allowed_bytes.Show()
				self._max_allowed_requests.Hide()

			elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
				self._max_allowed_bytes.Hide()
				self._max_allowed_requests.Show()

			self.Layout()

		def EventBandwidth( self, event ):
			self._UpdateEnabled()

		def GetValue( self ):
			bandwidth_type = self._bandwidth_type.GetChoice()

			time_delta = self._time_delta.GetValue()

			if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
				max_allowed = self._max_allowed_bytes.GetValue()

			elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
				max_allowed = self._max_allowed_requests.GetValue()

			return ( bandwidth_type, time_delta, max_allowed )

class BytesControl( wx.Panel ):
	def __init__( self, parent, initial_value = 65536 ):

		wx.Panel.__init__( self, parent )

		self._spin = wx.SpinCtrl( self, min = 0, max = 1048576 )

		width = ConvertTextToPixelWidth( self._spin, 12 )

		self._spin.SetSize( ( width, -1 ) )

		self._unit = BetterChoice( self )

		self._unit.Append( 'B', 1 )
		self._unit.Append( 'KB', 1024 )
		self._unit.Append( 'MB', 1024 * 1024 )
		self._unit.Append( 'GB', 1024 * 1024 * 1024 )

		#

		self.SetValue( initial_value )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._spin, CC.FLAGS_VCENTER )
		hbox.Add( self._unit, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def Bind( self, event_type, callback ):
		self._spin.Bind( wx.EVT_SPINCTRL, callback )

		self._unit.Bind( wx.EVT_CHOICE, callback )

	def Disable( self ):
		self._spin.Disable()
		self._unit.Disable()

	def Enable( self ):
		self._spin.Enable()
		self._unit.Enable()

	def GetSeparatedValue( self ):
		return ( self._spin.GetValue(), self._unit.GetChoice() )

	def GetValue( self ):
		return self._spin.GetValue() * self._unit.GetChoice()

	def SetSeparatedValue( self, value, unit ):
		return ( self._spin.SetValue( value ), self._unit.SelectClientData( unit ) )

	def SetValue( self, value ):
		max_unit = 1024 * 1024 * 1024

		unit = 1

		while value % 1024 == 0 and unit < max_unit:
			value //= 1024

			unit *= 1024

		self._spin.SetValue( value )
		self._unit.SelectClientData( unit )

class EditStringConverterPanel( EditPanel ):
	def __init__( self, parent, string_converter, example_string_override = None ):

		EditPanel.__init__( self, parent )

		transformations_panel = BetterListCtrlPanel( self )

		columns = [ ( '#', 3 ), ( 'transformation', 30 ), ( 'result', -1 ) ]

		self._transformations = BetterListCtrl( transformations_panel, 'string_converter_transformations', 7, 35, columns, self._ConvertTransformationToListCtrlTuples, delete_key_callback = self._DeleteTransformation, activation_callback = self._EditTransformation )

		transformations_panel.SetListCtrl( self._transformations )

		transformations_panel.AddButton( 'add', self._AddTransformation )
		transformations_panel.AddButton( 'edit', self._EditTransformation, enabled_only_on_selection = True )
		transformations_panel.AddDeleteButton()

		transformations_panel.AddSeparator()

		transformations_panel.AddButton( 'move up', self._MoveUp, enabled_check_func = self._CanMoveUp )
		transformations_panel.AddButton( 'move down', self._MoveDown, enabled_check_func = self._CanMoveDown )

		self._example_string = wx.TextCtrl( self )

		#

		self._transformations.AddDatas( [ ( i + 1, transformation_type, data ) for ( i, ( transformation_type, data ) ) in enumerate( string_converter.transformations ) ] )

		if example_string_override is None:
			self._example_string.SetValue( string_converter.example_string )

		else:
			self._example_string.SetValue( example_string_override )

		self._transformations.UpdateDatas() # to refresh, now they are all in the list

		self._transformations.Sort( 0 )

		#

		rows = []

		rows.append( ( 'example string: ', self._example_string ) )

		gridbox = WrapInGrid( self, rows )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( transformations_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		#

		self._example_string.Bind( wx.EVT_TEXT, self.EventUpdate )

	def _AddTransformation( self ):
		transformation_type = STRING_TRANSFORMATION_APPEND_TEXT
		data = ' extra text'

		with DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg:
			panel = self._TransformationPanel( dlg, transformation_type, data )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				number = self._transformations.GetItemCount() + 1

				( transformation_type, data ) = panel.GetValue()

				enumerated_transformation = ( number, transformation_type, data )

				self._transformations.AddDatas( ( enumerated_transformation, ) )

		self._transformations.UpdateDatas() # need to refresh string after the insertion, so the new row can be included in the parsing calcs

		self._transformations.Sort()

	def _CanMoveDown( self ):
		selected_data = self._transformations.GetData( only_selected = True )

		if len( selected_data ) == 1:
			( number, transformation_type, data ) = selected_data[0]

			if number < self._transformations.GetItemCount():
				return True

		return False

	def _CanMoveUp( self ):
		selected_data = self._transformations.GetData( only_selected = True )

		if len( selected_data ) == 1:
			( number, transformation_type, data ) = selected_data[0]

			if number > 1:
				return True

		return False

	def _ConvertTransformationToListCtrlTuples( self, transformation ):
		( number, transformation_type, data ) = transformation

		pretty_number = ToHumanInt( number )
		pretty_transformation = StringConverter.TransformationToString( ( transformation_type, data ) )

		string_converter = self._GetValue()

		try:
			pretty_result = MakeParsedTextPretty( string_converter.Convert( self._example_string.GetValue(), number ) )

		except StringConvertException as e:
			pretty_result = str( e )

		display_tuple = ( pretty_number, pretty_transformation, pretty_result )
		sort_tuple = ( number, number, number )

		return ( display_tuple, sort_tuple )

	def _DeleteTransformation( self ):
		if len( self._transformations.GetData( only_selected = True ) ) > 0:
			with DialogYesNo( self, 'Delete all selected?' ) as dlg:
				if dlg.ShowModal() == wx.ID_YES:
					self._transformations.DeleteSelected()

		# now we need to shuffle up any missing numbers

		num_rows = self._transformations.GetItemCount()

		i = 1
		search_i = i

		while i <= num_rows:
			try:
				transformation = self._GetTransformation( search_i )

				if search_i != i:
					self._transformations.DeleteDatas( ( transformation, ) )

					( search_i, transformation_type, data ) = transformation

					transformation = ( i, transformation_type, data )

					self._transformations.AddDatas( ( transformation, ) )

				i += 1
				search_i = i

			except DataMissing:
				search_i += 1

		self._transformations.UpdateDatas()

		self._transformations.Sort()

	def _EditTransformation( self ):
		selected_data = self._transformations.GetData( only_selected = True )

		for enumerated_transformation in selected_data:
			( number, transformation_type, data ) = enumerated_transformation

			with DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg:
				panel = self._TransformationPanel( dlg, transformation_type, data )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					self._transformations.DeleteDatas( ( enumerated_transformation, ) )

					( transformation_type, data ) = panel.GetValue()

					enumerated_transformation = ( number, transformation_type, data )

					self._transformations.AddDatas( ( enumerated_transformation, ) )

				else:
					break

		self._transformations.UpdateDatas()

		self._transformations.Sort()

	def _GetTransformation( self, desired_number ):
		for transformation in self._transformations.GetData():
			( number, transformation_type, data ) = transformation

			if number == desired_number:
				return transformation

		raise DataMissing()

	def _GetValue( self ):
		enumerated_transformations = list( self._transformations.GetData() )

		enumerated_transformations.sort()

		transformations = [ ( transformation_type, data ) for ( number, transformation_type, data ) in enumerated_transformations ]

		example_string = self._example_string.GetValue()

		string_converter = StringConverter( transformations, example_string )

		return string_converter

	def _MoveDown( self ):
		selected_transformation = self._transformations.GetData( only_selected = True )[0]

		( number, transformation_type, data ) = selected_transformation

		swap_transformation = self._GetTransformation( number + 1 )

		self._SwapTransformations( selected_transformation, swap_transformation )

		self._transformations.UpdateDatas()

		self._transformations.Sort()

	def _MoveUp( self ):
		selected_transformation = self._transformations.GetData( only_selected = True )[0]

		( number, transformation_type, data ) = selected_transformation

		swap_transformation = self._GetTransformation( number - 1 )

		self._SwapTransformations( selected_transformation, swap_transformation )

		self._transformations.UpdateDatas()

		self._transformations.Sort()

	def _SwapTransformations( self, one, two ):
		selected_data = self._transformations.GetData( only_selected = True )

		one_selected = one in selected_data
		two_selected = two in selected_data

		self._transformations.DeleteDatas( ( one, two ) )

		( number_1, transformation_type_1, data_1 ) = one
		( number_2, transformation_type_2, data_2 ) = two

		one = ( number_2, transformation_type_1, data_1 )
		two = ( number_1, transformation_type_2, data_2 )

		self._transformations.AddDatas( ( one, two ) )

		if one_selected:
			self._transformations.SelectDatas( ( one, ) )

		if two_selected:
			self._transformations.SelectDatas( ( two, ) )

	def EventUpdate( self, event ):
		self._transformations.UpdateDatas()

	def GetValue( self ):
		string_converter = self._GetValue()

		try:
			string_converter.Convert( self._example_string.GetValue() )

		except StringConvertException:
			raise VetoException( 'Please enter an example text that can be converted!' )

		return string_converter

	class _TransformationPanel( EditPanel ):
		def __init__( self, parent, transformation_type, data ):

			EditPanel.__init__( self, parent )

			self._transformation_type = BetterChoice( self )

			for t_type in ( STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, STRING_TRANSFORMATION_PREPEND_TEXT, STRING_TRANSFORMATION_APPEND_TEXT, STRING_TRANSFORMATION_ENCODE, STRING_TRANSFORMATION_DECODE, STRING_TRANSFORMATION_REVERSE, STRING_TRANSFORMATION_REGEX_SUB, STRING_TRANSFORMATION_DATE_DECODE, STRING_TRANSFORMATION_INTEGER_ADDITION ):
				self._transformation_type.Append( transformation_type_str_lookup[ t_type ], t_type )

			self._data_text = wx.TextCtrl( self )
			self._data_number = wx.SpinCtrl( self, min = 0, max = 65535 )
			self._data_encoding = BetterChoice( self )
			self._data_regex_pattern = wx.TextCtrl( self )
			self._data_regex_repl = wx.TextCtrl( self )
			self._data_date_link = BetterHyperLink( self, 'link to date info', 'https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior' )
			self._data_timezone = BetterChoice( self )
			self._data_timezone_offset = wx.SpinCtrl( self, min = -86400, max = 86400 )

			for e in ( 'hex', 'base64' ):
				self._data_encoding.Append( e, e )

			self._data_timezone.Append( 'GMT', HC.TIMEZONE_GMT )
			self._data_timezone.Append( 'Local', HC.TIMEZONE_LOCAL )
			self._data_timezone.Append( 'Offset', HC.TIMEZONE_OFFSET )

			#

			self._transformation_type.SelectClientData( transformation_type )

			self._UpdateDataControls()

			#

			if transformation_type in ( STRING_TRANSFORMATION_DECODE, STRING_TRANSFORMATION_ENCODE ):
				self._data_encoding.SelectClientData( data )

			elif transformation_type == STRING_TRANSFORMATION_REGEX_SUB:
				( pattern, repl ) = data

				self._data_regex_pattern.SetValue( pattern )
				self._data_regex_repl.SetValue( repl )

			elif transformation_type == STRING_TRANSFORMATION_DATE_DECODE:
				( phrase, timezone_type, timezone_offset ) = data

				self._data_text.SetValue( phrase )
				self._data_timezone.SelectClientData( timezone_type )
				self._data_timezone_offset.SetValue( timezone_offset )

			elif data is not None:
				if isinstance( data, int ):
					self._data_number.SetValue( data )

				else:
					self._data_text.SetValue( data )

			#

			rows = []

			rows.append( ( 'string data: ', self._data_text ) )
			rows.append( ( 'number data: ', self._data_number ) )
			rows.append( ( 'encoding data: ', self._data_encoding ) )
			rows.append( ( 'regex pattern: ', self._data_regex_pattern ) )
			rows.append( ( 'regex replacement: ', self._data_regex_repl ) )
			rows.append( ( 'date info: ', self._data_date_link ) )
			rows.append( ( 'date timezone: ', self._data_timezone ) )
			rows.append( ( 'timezone offset: ', self._data_timezone_offset ) )

			gridbox = WrapInGrid( self, rows )

			vbox = wx.BoxSizer( wx.VERTICAL )

			vbox.Add( self._transformation_type, CC.FLAGS_EXPAND_PERPENDICULAR )
			vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

			self.SetSizer( vbox )

			#

			self._transformation_type.Bind( wx.EVT_CHOICE, self.EventChoice )
			self._data_timezone.Bind( wx.EVT_CHOICE, self.EventChoice )

		def _UpdateDataControls( self ):
			self._data_text.Disable()
			self._data_number.Disable()
			self._data_encoding.Disable()
			self._data_regex_pattern.Disable()
			self._data_regex_repl.Disable()
			self._data_timezone.Disable()
			self._data_timezone_offset.Disable()

			transformation_type = self._transformation_type.GetChoice()

			if transformation_type in ( STRING_TRANSFORMATION_ENCODE, STRING_TRANSFORMATION_DECODE ):
				self._data_encoding.Enable()

			elif transformation_type in ( STRING_TRANSFORMATION_PREPEND_TEXT, STRING_TRANSFORMATION_APPEND_TEXT, STRING_TRANSFORMATION_DATE_DECODE ):
				self._data_text.Enable()

				if transformation_type == STRING_TRANSFORMATION_DATE_DECODE:
					self._data_timezone.Enable()

					if self._data_timezone.GetChoice() == HC.TIMEZONE_OFFSET:
						self._data_timezone_offset.Enable()

			elif transformation_type in ( STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, STRING_TRANSFORMATION_INTEGER_ADDITION ):
				self._data_number.Enable()

				if transformation_type == STRING_TRANSFORMATION_INTEGER_ADDITION:
					self._data_number.SetMin( -65535 )

				else:
					self._data_number.SetMin( 0 )

			elif transformation_type == STRING_TRANSFORMATION_REGEX_SUB:
				self._data_regex_pattern.Enable()
				self._data_regex_repl.Enable()

		def EventChoice( self, event ):
			self._UpdateDataControls()

		def GetValue( self ):
			transformation_type = self._transformation_type.GetChoice()

			if transformation_type in ( STRING_TRANSFORMATION_ENCODE, STRING_TRANSFORMATION_DECODE ):
				data = self._data_encoding.GetChoice()

			elif transformation_type in ( STRING_TRANSFORMATION_PREPEND_TEXT, STRING_TRANSFORMATION_APPEND_TEXT ):
				data = self._data_text.GetValue()

			elif transformation_type in ( STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, STRING_TRANSFORMATION_INTEGER_ADDITION ):
				data = self._data_number.GetValue()

			elif transformation_type == STRING_TRANSFORMATION_REGEX_SUB:
				pattern = self._data_regex_pattern.GetValue()
				repl = self._data_regex_repl.GetValue()

				data = ( pattern, repl )

			elif transformation_type == STRING_TRANSFORMATION_DATE_DECODE:
				phrase = self._data_text.GetValue()
				timezone_time = self._data_timezone.GetChoice()
				timezone_offset = self._data_timezone_offset.GetValue()

				data = ( phrase, timezone_time, timezone_offset )

			else:
				data = None

			return ( transformation_type, data )

class EditStringMatchPanel( EditPanel ):
	def __init__( self, parent, string_match = None ):

		EditPanel.__init__( self, parent )

		if string_match is None:
			string_match = StringMatch()

		self._match_type = BetterChoice( self )

		self._match_type.Append( 'any characters', STRING_MATCH_ANY )
		self._match_type.Append( 'fixed characters', STRING_MATCH_FIXED )
		self._match_type.Append( 'character set', STRING_MATCH_FLEXIBLE )
		self._match_type.Append( 'regex', STRING_MATCH_REGEX )

		self._match_value_text_input = wx.TextCtrl( self )

		self._match_value_flexible_input = BetterChoice( self )

		self._match_value_flexible_input.Append( 'alphabetic characters (a-zA-Z)', ALPHA )
		self._match_value_flexible_input.Append( 'alphanumeric characters (a-zA-Z0-9)', ALPHANUMERIC )
		self._match_value_flexible_input.Append( 'numeric characters (0-9)', NUMERIC )

		self._min_chars = NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )
		self._max_chars = NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )

		self._example_string = wx.TextCtrl( self )

		self._example_string_matches = BetterStaticText( self )

		#

		self.SetValue( string_match )

		#

		rows = []

		rows.append( ( 'match type: ', self._match_type ) )
		rows.append( ( 'match text: ', self._match_value_text_input ) )
		rows.append( ( 'match value (character set ): ', self._match_value_flexible_input ) )
		rows.append( ( 'minumum allowed number of characters: ', self._min_chars ) )
		rows.append( ( 'maximum allowed number of characters: ', self._max_chars ) )
		rows.append( ( 'example string: ', self._example_string ) )

		gridbox = WrapInGrid( self, rows )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		#

		self._match_type.Bind( wx.EVT_CHOICE, self.EventUpdate )
		self._match_value_text_input.Bind( wx.EVT_TEXT, self.EventUpdate )
		self._match_value_flexible_input.Bind( wx.EVT_CHOICE, self.EventUpdate )
		self._min_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
		self._max_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
		self._example_string.Bind( wx.EVT_TEXT, self.EventUpdate )

	def _GetValue( self ):
		match_type = self._match_type.GetChoice()

		if match_type == STRING_MATCH_ANY:
			match_value = ''

		elif match_type == STRING_MATCH_FLEXIBLE:
			match_value = self._match_value_flexible_input.GetChoice()

		else:
			match_value = self._match_value_text_input.GetValue()

		min_chars = self._min_chars.GetValue()
		max_chars = self._max_chars.GetValue()

		example_string = self._example_string.GetValue()

		string_match = StringMatch( match_type = match_type, match_value = match_value, min_chars = min_chars, max_chars = max_chars, example_string = example_string )

		return string_match

	def _UpdateControls( self ):
		match_type = self._match_type.GetChoice()

		if match_type == STRING_MATCH_ANY:
			self._match_value_text_input.Disable()
			self._match_value_flexible_input.Disable()

		elif match_type == STRING_MATCH_FLEXIBLE:
			self._match_value_text_input.Disable()
			self._match_value_flexible_input.Enable()

		else:
			self._match_value_text_input.Enable()
			self._match_value_flexible_input.Disable()

		if match_type == STRING_MATCH_FIXED:
			self._min_chars.SetValue( None )
			self._max_chars.SetValue( None )

			self._min_chars.Disable()
			self._max_chars.Disable()

			self._example_string.ChangeValue( self._match_value_text_input.GetValue() )

			self._example_string_matches.SetLabelText( '' )

		else:
			self._min_chars.Enable()
			self._max_chars.Enable()

			string_match = self._GetValue()

			try:
				string_match.Test( self._example_string.GetValue() )

				self._example_string_matches.SetLabelText( 'Example matches ok!' )
				self._example_string_matches.SetForegroundColour( ( 0, 128, 0 ) )

			except StringMatchException as e:
				reason = str( e )

				self._example_string_matches.SetLabelText( 'Example does not match - ' + reason )
				self._example_string_matches.SetForegroundColour( ( 128, 0, 0 ) )

	def EventUpdate( self, event ):
		self._UpdateControls()

		event.Skip()

	def GetValue( self ):
		string_match = self._GetValue()

		try:
			string_match.Test( self._example_string.GetValue() )

		except StringMatchException:
			raise VetoException( 'Please enter an example text that matches the given rules!' )

		return string_match

	def SetValue( self, string_match ):
		( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple()

		self._match_type.SelectClientData( match_type )

		if match_type == STRING_MATCH_FLEXIBLE:
			self._match_value_flexible_input.SelectClientData( match_value )

		else:
			self._match_value_flexible_input.SelectClientData( ALPHA )

			self._match_value_text_input.SetValue( match_value )

		self._min_chars.SetValue( min_chars )
		self._max_chars.SetValue( max_chars )

		self._example_string.SetValue( example_string )

		self._UpdateControls()

class NoneableBytesControl( wx.Panel ):
	def __init__( self, parent, initial_value = 65536, none_label = 'no limit' ):

		wx.Panel.__init__( self, parent )

		self._bytes = BytesControl( self )

		self._none_checkbox = wx.CheckBox( self, label = none_label )

		#

		self.SetValue( initial_value )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._bytes, CC.FLAGS_SIZER_VCENTER )
		hbox.Add( self._none_checkbox, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		#

		self._none_checkbox.Bind( wx.EVT_CHECKBOX, self.EventNoneChecked )

	def _UpdateEnabled( self ):
		if self._none_checkbox.GetValue():
			self._bytes.Disable()

		else:
			self._bytes.Enable()

	def EventNoneChecked( self, event ):
		self._UpdateEnabled()

	def Bind( self, event_type, callback ):
		self._bytes.Bind( wx.EVT_SPINCTRL, callback )

		self._none_checkbox.Bind( wx.EVT_CHECKBOX, callback )

	def GetValue( self ):
		if self._none_checkbox.GetValue():
			return None

		else:
			return self._bytes.GetValue()

	def SetToolTip( self, text ):
		wx.Panel.SetToolTip( self, text )

		for c in self.GetChildren():
			c.SetToolTip( text )

	def SetValue( self, value ):
		if value is None:
			self._none_checkbox.SetValue( True )

		else:
			self._none_checkbox.SetValue( False )

			self._bytes.SetValue( value )

		self._UpdateEnabled()

class NetworkJobControl( wx.Panel ):
	def __init__( self, parent ):
		wx.Panel.__init__( self, parent, style = wx.BORDER_DOUBLE )

		self._network_job = None
		self._download_started = False

		self._auto_override_bandwidth_rules = False

		self._left_text = BetterStaticText( self, style = wx.ST_ELLIPSIZE_END )
		self._right_text = BetterStaticText( self, style = wx.ALIGN_RIGHT )

		self._last_right_min_width = ( -1, -1 )

		self._gauge = Gauge( self )

		self._cog_button = BetterBitmapButton( self, CC.GlobalBMPs.cog, self._ShowCogMenu )
		self._cancel_button = BetterBitmapButton( self, CC.GlobalBMPs.stop, self.Cancel )

		#

		self._Update()

		#

		st_hbox = wx.BoxSizer( wx.HORIZONTAL )

		st_hbox.Add( self._left_text, CC.FLAGS_EXPAND_BOTH_WAYS )
		st_hbox.Add( self._right_text, CC.FLAGS_VCENTER )

		left_vbox = wx.BoxSizer( wx.VERTICAL )

		left_vbox.Add( st_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
		left_vbox.Add( self._gauge, CC.FLAGS_EXPAND_PERPENDICULAR )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( left_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
		hbox.Add( self._cog_button, CC.FLAGS_VCENTER )
		hbox.Add( self._cancel_button, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def _ShowCogMenu( self ):
		menu = wx.Menu()

		if self._network_job is not None:
			if self._network_job.ObeysBandwidth():
				AppendMenuItem( self, menu, 'override bandwidth rules for this job', 'Tell the current job to ignore existing bandwidth rules and go ahead anyway.', self._network_job.OverrideBandwidth )

			if not self._network_job.TokensOK():
				AppendMenuItem( self, menu, 'override gallery slot requirements for this job', 'Force-allow this download to proceed, ignoring the normal gallery wait times.', self._network_job.OverrideToken )

			AppendSeparator( menu )

		AppendMenuCheckItem( self, menu, 'auto-override bandwidth rules for all jobs here after five seconds', 'Ignore existing bandwidth rules for all jobs under this control, instead waiting a flat five seconds.', self._auto_override_bandwidth_rules, self.FlipAutoOverrideBandwidth )

		HG.client_controller.PopupMenu( self._cog_button, menu )

	def _OverrideBandwidthIfAppropriate( self ):
		if self._network_job is None or self._network_job.NoEngineYet():
			return

		else:
			if self._auto_override_bandwidth_rules and TimeHasPassed( self._network_job.GetCreationTime() + 5 ):
				self._network_job.OverrideBandwidth()

	def _Update( self ):
		if self._network_job is None or self._network_job.NoEngineYet():
			self._left_text.SetLabelText( '' )
			self._right_text.SetLabelText( '' )
			self._gauge.SetRange( 1 )
			self._gauge.SetValue( 0 )

			can_cancel = False

		else:
			if self._network_job.IsDone():
				can_cancel = False

			else:
				can_cancel = True

			( status_text, current_speed, bytes_read, bytes_to_read ) = self._network_job.GetStatus()

			self._left_text.SetLabelText( status_text )

			if not self._download_started and current_speed > 0:
				self._download_started = True

			speed_text = ''

			if self._download_started and not self._network_job.HasError():
				if bytes_read is not None:
					if bytes_to_read is not None and bytes_read != bytes_to_read:
						speed_text += ConvertValueRangeToBytes( bytes_read, bytes_to_read )

					else:
						speed_text += ToHumanBytes( bytes_read )

				if current_speed != bytes_to_read: # if it is a real quick download, just say its size

					speed_text += ' ' + ToHumanBytes( current_speed ) + '/s'

			self._right_text.SetLabelText( speed_text )

			right_width = ConvertTextToPixelWidth( self._right_text, len( speed_text ) )

			right_min_size = ( right_width, -1 )

			if right_min_size != self._last_right_min_width:
				self._last_right_min_width = right_min_size

				self._right_text.SetMinSize( right_min_size )

				self.Layout()

			self._gauge.SetRange( bytes_to_read )
			self._gauge.SetValue( bytes_read )

		if can_cancel:
			if not self._cancel_button.IsEnabled():
				self._cancel_button.Enable()

		else:
			if self._cancel_button.IsEnabled():
				self._cancel_button.Disable()

	def Cancel( self ):
		if self._network_job is not None:
			self._network_job.Cancel( 'Cancelled by user.' )

	def ClearNetworkJob( self ):
		self.SetNetworkJob( None )

	def FlipAutoOverrideBandwidth( self ):
		self._auto_override_bandwidth_rules = not self._auto_override_bandwidth_rules

	def SetNetworkJob( self, network_job ):
		if network_job is None:
			if self._network_job is not None:
				self._network_job = None

				self._Update()

				HG.client_controller.gui.UnregisterUIUpdateWindow( self )

		else:
			if self._network_job != network_job:
				self._network_job = network_job
				self._download_started = False

				HG.client_controller.gui.RegisterUIUpdateWindow( self )

	def TIMERUIUpdate( self ):
		self._OverrideBandwidthIfAppropriate()

		if HG.client_controller.gui.IShouldRegularlyUpdate( self ):
			self._Update()

( StringConverterEvent, EVT_STRING_CONVERTER ) = wx.lib.newevent.NewCommandEvent()

class StringConverterButton( BetterButton ):
	def __init__( self, parent, string_converter ):
		BetterButton.__init__( self, parent, 'edit string converter', self._Edit )

		self._string_converter = string_converter

		self._example_string_override = None

		self._UpdateLabel()

	def _Edit( self ):
		with DialogEdit( self, 'edit string converter', frame_key = 'deeply_nested_dialog' ) as dlg:
			panel = EditStringConverterPanel( dlg, self._string_converter, example_string_override = self._example_string_override )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				self._string_converter = panel.GetValue()

				self._UpdateLabel()

		wx.QueueEvent( self.GetEventHandler(), StringConverterEvent( -1 ) )

	def _UpdateLabel( self ):
		num_rules = len( self._string_converter.transformations )

		if num_rules == 0:
			label = 'no string transformations'

		else:
			label = ToHumanInt( num_rules ) + ' string transformations'

		self.SetLabelText( label )

	def GetValue( self ):
		return self._string_converter

	def SetExampleString( self, example_string ):
		self._example_string_override = example_string

	def SetValue( self, string_converter ):
		self._string_converter = string_converter

		self._UpdateLabel()

class StringMatchButton( BetterButton ):
	def __init__( self, parent, string_match ):
		BetterButton.__init__( self, parent, 'edit string match', self._Edit )

		self._string_match = string_match

		self._UpdateLabel()

	def _Edit( self ):
		with DialogEdit( self, 'edit string match', frame_key = 'deeply_nested_dialog' ) as dlg:
			panel = EditStringMatchPanel( dlg, self._string_match )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				self._string_match = panel.GetValue()

				self._UpdateLabel()

	def _UpdateLabel( self ):
		label = self._string_match.ToString()

		self.SetLabelText( label )

	def GetValue( self ):
		return self._string_match

	def SetValue( self, string_match ):
		self._string_match = string_match

		self._UpdateLabel()

class StringMatchToStringMatchDictControl( wx.Panel ):
	def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key' ):
		wx.Panel.__init__( self, parent )

		self._key_name = key_name

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ]

		self._listctrl = BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		listctrl_panel.SetListCtrl( self._listctrl )

		listctrl_panel.AddButton( 'add', self._Add )
		listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		listctrl_panel.AddDeleteButton()

		#

		self._listctrl.AddDatas( list( initial_dict.items() ) )

		self._listctrl.Sort()

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

	def _ConvertDataToListCtrlTuples( self, data ):
		( key_string_match, value_string_match ) = data

		pretty_key = key_string_match.ToString()
		pretty_value = value_string_match.ToString()

		display_tuple = ( pretty_key, pretty_value )
		sort_tuple = ( pretty_key, pretty_value )

		return ( display_tuple, sort_tuple )

	def _Add( self ):
		with DialogEdit( self, 'edit ' + self._key_name ) as dlg:
			string_match = StringMatch()

			panel = EditStringMatchPanel( dlg, string_match )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				key_string_match = panel.GetValue()

			else:
				return

		with DialogEdit( self, 'edit match' ) as dlg:
			string_match = StringMatch()

			panel = EditStringMatchPanel( dlg, string_match )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				value_string_match = panel.GetValue()

				data = ( key_string_match, value_string_match )

				self._listctrl.AddDatas( ( data, ) )

	def _Edit( self ):
		for data in self._listctrl.GetData( only_selected = True ):
			( key_string_match, value_string_match ) = data

			with DialogEdit( self, 'edit ' + self._key_name ) as dlg:
				panel = EditStringMatchPanel( dlg, key_string_match )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					key_string_match = panel.GetValue()

				else:
					break

			with DialogEdit( self, 'edit match' ) as dlg:
				panel = EditStringMatchPanel( dlg, value_string_match )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					value_string_match = panel.GetValue()

				else:
					break

			self._listctrl.DeleteDatas( ( data, ) )

			edited_data = ( key_string_match, value_string_match )

			self._listctrl.AddDatas( ( edited_data, ) )

		self._listctrl.Sort()

	def GetValue( self ):
		value_dict = dict( self._listctrl.GetData() )

		return value_dict

class StringToStringDictButton( BetterButton ):
	def __init__( self, parent, label ):
		BetterButton.__init__( self, parent, label, self._Edit )

		self._value = {}

	def _Edit( self ):
		with DialogEdit( self, 'edit string dictionary' ) as dlg:
			panel = EditSingleCtrlPanel( dlg )

			control = StringToStringDictControl( panel, self._value )

			panel.SetControl( control )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				self._value = control.GetValue()

	def GetValue( self ):
		return self._value

	def SetValue( self, value ):
		self._value = value

class StringToStringDictControl( wx.Panel ):
	def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key', value_name = 'value' ):
		wx.Panel.__init__( self, parent )

		self._key_name = key_name
		self._value_name = value_name

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( self._key_name, 20 ), ( self._value_name, -1 ) ]

		self._listctrl = BetterListCtrl( listctrl_panel, 'key_to_value', min_height, 36, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		listctrl_panel.SetListCtrl( self._listctrl )

		listctrl_panel.AddButton( 'add', self._Add )
		listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		listctrl_panel.AddDeleteButton()

		#

		self._listctrl.AddDatas( list( initial_dict.items() ) )

		self._listctrl.Sort()

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

	def _ConvertDataToListCtrlTuples( self, data ):
		( key, value ) = data

		display_tuple = ( key, value )
		sort_tuple = ( key, value )

		return ( display_tuple, sort_tuple )

	def _Add( self ):
		with DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				key = dlg.GetValue()

				if key in self._GetExistingKeys():
					wx.MessageBox( 'That ' + self._key_name + ' already exists!' )

					return

				with DialogTextEntry( self, 'enter the ' + self._value_name, allow_blank = True ) as dlg:
					if dlg.ShowModal() == wx.ID_OK:
						value = dlg.GetValue()

						data = ( key, value )

						self._listctrl.AddDatas( ( data, ) )

	def _Edit( self ):
		for data in self._listctrl.GetData( only_selected = True ):
			( key, value ) = data

			with DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg:
				if dlg.ShowModal() == wx.ID_OK:
					edited_key = dlg.GetValue()

					if edited_key != key and edited_key in self._GetExistingKeys():
						wx.MessageBox( 'That ' + self._key_name + ' already exists!' )

						break

				else:
					break

			with DialogTextEntry( self, 'edit the ' + self._value_name, default = value, allow_blank = True ) as dlg:
				if dlg.ShowModal() == wx.ID_OK:
					edited_value = dlg.GetValue()

				else:
					break

			self._listctrl.DeleteDatas( ( data, ) )

			edited_data = ( edited_key, edited_value )

			self._listctrl.AddDatas( ( edited_data, ) )

		self._listctrl.Sort()

	def _GetExistingKeys( self ):
		return { key for ( key, value ) in self._listctrl.GetData() }

	def GetValue( self ):
		value_dict = dict( self._listctrl.GetData() )

		return value_dict

class StringToStringMatchDictControl( wx.Panel ):
	def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key' ):
		wx.Panel.__init__( self, parent )

		self._key_name = key_name

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ]

		self._listctrl = BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		listctrl_panel.SetListCtrl( self._listctrl )

		listctrl_panel.AddButton( 'add', self._Add )
		listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		listctrl_panel.AddDeleteButton()

		#

		self._listctrl.AddDatas( list( initial_dict.items() ) )

		self._listctrl.Sort()

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

	def _ConvertDataToListCtrlTuples( self, data ):
		( key, string_match ) = data

		pretty_string_match = string_match.ToString()

		display_tuple = ( key, pretty_string_match )
		sort_tuple = ( key, pretty_string_match )

		return ( display_tuple, sort_tuple )

	def _Add( self ):
		with DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				key = dlg.GetValue()

				if key in self._GetExistingKeys():
					wx.MessageBox( 'That ' + self._key_name + ' already exists!' )

					return

				with DialogEdit( self, 'edit match' ) as dlg:
					string_match = StringMatch()

					panel = EditStringMatchPanel( dlg, string_match )

					dlg.SetPanel( panel )

					if dlg.ShowModal() == wx.ID_OK:
						string_match = panel.GetValue()

						data = ( key, string_match )

						self._listctrl.AddDatas( ( data, ) )

	def _Edit( self ):
		for data in self._listctrl.GetData( only_selected = True ):
			( key, string_match ) = data

			with DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg:
				if dlg.ShowModal() == wx.ID_OK:
					edited_key = dlg.GetValue()

					if edited_key != key and edited_key in self._GetExistingKeys():
						wx.MessageBox( 'That ' + self._key_name + ' already exists!' )

						break

				else:
					break

			with DialogEdit( self, 'edit match' ) as dlg:
				string_match = StringMatch()

				panel = EditStringMatchPanel( dlg, string_match )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					edited_string_match = panel.GetValue()

				else:
					break

			self._listctrl.DeleteDatas( ( data, ) )

			edited_data = ( edited_key, edited_string_match )

			self._listctrl.AddDatas( ( edited_data, ) )

		self._listctrl.Sort()

	def _GetExistingKeys( self ):
		return { key for ( key, value ) in self._listctrl.GetData() }

	def GetValue( self ):
		value_dict = dict( self._listctrl.GetData() )

		return value_dict

class TextAndPasteCtrl( wx.Panel ):
	def __init__( self, parent, add_callable, allow_empty_input = False ):
		self._add_callable = add_callable
		self._allow_empty_input = allow_empty_input

		wx.Panel.__init__( self, parent )

		self._text_input = wx.TextCtrl( self, style = wx.TE_PROCESS_ENTER )
		self._text_input.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )

		self._paste_button = BetterBitmapButton( self, CC.GlobalBMPs.paste, self._Paste )
		self._paste_button.SetToolTip( 'Paste multiple inputs from the clipboard. Assumes the texts are newline-separated.' )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._text_input, CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( self._paste_button, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def _Paste( self ):
		raw_text = HG.client_controller.GetClipboardText()

		try:
			texts = [ text for text in DeserialiseNewlinedTexts( raw_text ) ]

			if not self._allow_empty_input:
				texts = [ text for text in texts if text != '' ]

			if len( texts ) > 0:
				self._add_callable( texts )

		except Exception:
			wx.MessageBox( 'I could not understand what was in the clipboard' )

	def EventKeyDown( self, event ):
		( modifier, key ) = ConvertKeyEventToSimpleTuple( event )

		if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
			text = self._text_input.GetValue()

			text = StripIOInputLine( text )

			if text == '' and not self._allow_empty_input:
				return

			self._add_callable( ( text, ) )

			self._text_input.SetValue( '' )

		else:
			event.Skip()

	def GetValue( self ):
		return self._text_input.GetValue()

	def SetValue( self, text ):
		self._text_input.SetValue( text )


code:
import os, re, queue, threading, time, wx, wx.lib.agw.customtreectrl, collections

from lib.GUI.ACDropdown import AutoCompleteDropdownTagsWrite
from lib.GUI.Common import BetterBitmapButton, BetterButton, BetterHyperLink, \
	BetterStaticText, CheckboxManagerOptions, MenuBitmapButton, NoneableSpinCtrl, \
	RegexButton, SaneMultilineTextCtrl, SetBitmapButtonBitmap, StaticBox, TextAndGauge, \
	ThreadToGUIUpdater, WrapInGrid, BetterChoice
from lib.GUI.Dialogs.Quick import SelectFromList
from lib.GUI.DragDrop import FileDropTarget
from lib.GUI.Frames import ShowKeys
from lib.GUI.Import import EditLocalImportFilenameTaggingPanel, FileImportOptionsButton
from lib.GUI.ListBoxes import ListBoxTagsStringsAddRemove
from lib.GUI.ListCtrl import BetterListCtrl, BetterListCtrlPanel
from lib.GUI.Shortcuts import ConvertKeyEventToSimpleTuple
from lib.GUI.TopLevelWindows import DialogEdit, GetDisplaySize

from lib.Handling import GetMime

from lib.Hydrus.Data import ConvertValueRangeToPrettyString, GetNow, GetTimeDeltaUntilTime, \
	Print, ToHumanBytes, ToHumanInt
from lib.Hydrus.Exceptions import CancelledException
from lib.Hydrus.Paths import PathIsFree
from lib.Hydrus.Paths.Local import GetAllPaths
from lib.Hydrus.Tags.Local import ServiceKeysToTags
from lib.Hydrus.Threading import JobKey

from lib.Networking.NATPunch import GetExternalIP

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

# Option Enums

ID_NULL = wx.NewId()

ID_TIMER_UPDATE = wx.NewId()

### FUNCTIONS ###

def SelectServiceKey( service_types = HC.ALL_SERVICES, service_keys = None, unallowed = None ):
	if service_keys is None:
		services = HG.client_controller.services_manager.GetServices( service_types )

		service_keys = [ service.GetServiceKey() for service in services ]

	if unallowed is not None:
		service_keys.difference_update( unallowed )

	if len( service_keys ) == 0:
		return None

	elif len( service_keys ) == 1:
		( service_key, ) = service_keys

		return service_key

	else:
		services = { HG.client_controller.services_manager.GetService( service_key ) for service_key in service_keys }

		choice_tuples = [ ( service.GetName(), service.GetServiceKey() ) for service in services ]

		try:
			service_key = SelectFromList( HG.client_controller.GetGUI(), 'select service', choice_tuples )

			return service_key

		except CancelledException:
			return None

class Dialog( wx.Dialog ):
	def __init__( self, parent, title, style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, position = 'topleft' ):
		if parent is not None and position == 'topleft':
			if isinstance( parent, wx.TopLevelWindow ):
				parent_tlp = parent

			else:
				parent_tlp = parent.GetTopLevelParent()

			( pos_x, pos_y ) = parent_tlp.GetPosition()

			pos = ( pos_x + 50, pos_y + 50 )

		else:
			pos = wx.DefaultPosition

		if not HC.PLATFORM_LINUX and parent is not None:
			style |= wx.FRAME_FLOAT_ON_PARENT

		wx.Dialog.__init__( self, parent, title = title, style = style, pos = pos )

		self._new_options = HG.client_controller.new_options

		self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )

		self.SetIcon( HG.client_controller.frame_icon )

		self.Bind( wx.EVT_BUTTON, self.EventDialogButton )
		self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )

		if parent is not None and position == 'center':
			wx.CallAfter( self.Center )

		HG.client_controller.ResetIdleTimer()

	def EventCharHook( self, event ):
		( modifier, key ) = ConvertKeyEventToSimpleTuple( event )

		if key == wx.WXK_ESCAPE:
			self.EndModal( wx.ID_CANCEL )

		else:
			event.Skip()

	def EventDialogButton( self, event ):
		if self.IsModal():
			self.EndModal( event.GetId() )

	def SetInitialSize( self, size ):
		( width, height ) = size

		( display_width, display_height ) = GetDisplaySize( self )

		width = min( display_width, width )
		height = min( display_height, height )

		wx.Dialog.SetInitialSize( self, ( width, height ) )

		min_width = min( 240, width )
		min_height = min( 240, height )

		self.SetMinSize( ( min_width, min_height ) )

class DialogChooseNewServiceMethod( Dialog ):
	def __init__( self, parent ):

		Dialog.__init__( self, parent, 'how to set up the account?', position = 'center' )

		register_message = 'I want to initialise a new account with the server. I have a registration key (a key starting with \'r\').'

		self._register = wx.Button( self, label = register_message )
		self._register.Bind( wx.EVT_BUTTON, self.EventRegister )

		setup_message = 'The account is already initialised; I just want to add it to this client. I have a normal access key.'

		self._setup = wx.Button( self, id = wx.ID_OK, label = setup_message )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._register, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( wx.StaticText( self, label = '-or-', style = wx.ALIGN_CENTER ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._setup, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		self._should_register = False

		wx.CallAfter( self._register.SetFocus )

	def EventRegister( self, event ):
		self._should_register = True

		self.EndModal( wx.ID_OK )

	def GetRegister( self ): return self._should_register

class DialogCommitInterstitialFiltering( Dialog ):
	def __init__( self, parent, label ):

		Dialog.__init__( self, parent, 'commit and continue?', position = 'center' )

		self._commit = wx.Button( self, id = wx.ID_YES, label = 'commit and continue' )
		self._commit.SetForegroundColour( ( 0, 128, 0 ) )

		self._back = wx.Button( self, id = wx.ID_CANCEL, label = 'go back' )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( wx.StaticText( self, label = label, style = wx.ALIGN_CENTER ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._commit, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( wx.StaticText( self, label = '-or-', style = wx.ALIGN_CENTER ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._back, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._commit.SetFocus )

class DialogFinishFiltering( Dialog ):
	def __init__( self, parent, label ):

		Dialog.__init__( self, parent, 'are you sure?', position = 'center' )

		self._commit = BetterButton( self, 'commit', self.EndModal, wx.ID_YES )
		self._commit.SetForegroundColour( ( 0, 128, 0 ) )

		self._forget = BetterButton( self, 'forget', self.EndModal, wx.ID_NO )
		self._forget.SetForegroundColour( ( 128, 0, 0 ) )

		self._back = BetterButton( self, 'back to filtering', self.EndModal, wx.ID_CANCEL )
		self._back.SetId( wx.ID_CANCEL )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._commit, CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( self._forget, CC.FLAGS_EXPAND_BOTH_WAYS )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( wx.StaticText( self, label = label, style = wx.ALIGN_CENTER ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( wx.StaticText( self, label = '-or-', style = wx.ALIGN_CENTER ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._back, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._commit.SetFocus )

class DialogGenerateNewAccounts( Dialog ):
	def __init__( self, parent, service_key ):

		Dialog.__init__( self, parent, 'configure new accounts' )

		self._service_key = service_key

		self._num = wx.SpinCtrl( self, min = 1, max = 10000, size = ( 80, -1 ) )

		self._account_types = BetterChoice( self )

		self._lifetime = BetterChoice( self )

		self._ok = wx.Button( self, label = 'OK' )
		self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._num.SetValue( 1 )

		service = HG.client_controller.services_manager.GetService( service_key )

		response = service.Request( HC.GET, 'account_types' )

		account_types = response[ 'account_types' ]

		for account_type in account_types:
			self._account_types.Append( account_type.GetTitle(), account_type )

		self._account_types.Select( 0 )

		for ( s, value ) in HC.lifetimes:
			self._lifetime.Append( s, value )

		self._lifetime.SetSelection( 3 ) # one year

		#

		ctrl_box = wx.BoxSizer( wx.HORIZONTAL )

		ctrl_box.Add( BetterStaticText( self, 'generate' ), CC.FLAGS_VCENTER )
		ctrl_box.Add( self._num, CC.FLAGS_VCENTER )
		ctrl_box.Add( self._account_types, CC.FLAGS_VCENTER )
		ctrl_box.Add( BetterStaticText( self, 'accounts, to expire in' ), CC.FLAGS_VCENTER )
		ctrl_box.Add( self._lifetime, CC.FLAGS_VCENTER )

		b_box = wx.BoxSizer( wx.HORIZONTAL )
		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( ctrl_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._ok.SetFocus )

	def EventOK( self, event ):
		num = self._num.GetValue()

		account_type = self._account_types.GetChoice()

		account_type_key = account_type.GetAccountTypeKey()

		lifetime = self._lifetime.GetChoice()

		if lifetime is None:
			expires = None

		else:
			expires = GetNow() + lifetime

		service = HG.client_controller.services_manager.GetService( self._service_key )

		try:
			request_args = { 'num' : num, 'account_type_key' : account_type_key }

			if expires is not None:
				request_args[ 'expires' ] = expires

			response = service.Request( HC.GET, 'registration_keys', request_args )

			registration_keys = response[ 'registration_keys' ]

			ShowKeys( 'registration', registration_keys )

		finally:
			self.EndModal( wx.ID_OK )

class DialogInputLocalBooruShare( Dialog ):
	def __init__( self, parent, share_key, name, text, timeout, hashes, new_share = False ):

		Dialog.__init__( self, parent, 'configure local booru share' )

		self._name = wx.TextCtrl( self )

		self._text = SaneMultilineTextCtrl( self )
		self._text.SetMinSize( ( -1, 100 ) )

		message = 'expires in'

		self._timeout_number = NoneableSpinCtrl( self, message, none_phrase = 'no expiration', max = 1000000, multiplier = 1 )

		self._timeout_multiplier = BetterChoice( self )
		self._timeout_multiplier.Append( 'minutes', 60 )
		self._timeout_multiplier.Append( 'hours', 60 * 60 )
		self._timeout_multiplier.Append( 'days', 60 * 60 * 24 )

		self._copy_internal_share_link = wx.Button( self, label = 'copy internal share link' )
		self._copy_internal_share_link.Bind( wx.EVT_BUTTON, self.EventCopyInternalShareURL )

		self._copy_external_share_link = wx.Button( self, label = 'copy external share link' )
		self._copy_external_share_link.Bind( wx.EVT_BUTTON, self.EventCopyExternalShareURL )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'ok' )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._share_key = share_key
		self._name.SetValue( name )
		self._text.SetValue( text )

		if timeout is None:
			self._timeout_number.SetValue( None )

			self._timeout_multiplier.SelectClientData( 60 )

		else:
			time_left = GetTimeDeltaUntilTime( timeout )

			if time_left < 60 * 60 * 12:
				time_value = 60
			elif time_left < 60 * 60 * 24 * 7:
				time_value = 60 * 60
			else:
				time_value = 60 * 60 * 24

			self._timeout_number.SetValue( time_left // time_value )

			self._timeout_multiplier.SelectClientData( time_value )

		self._hashes = hashes

		self._service = HG.client_controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )

		internal_port = self._service.GetPort()

		if internal_port is None:
			self._copy_internal_share_link.Disable()
			self._copy_external_share_link.Disable()

		#

		rows = []

		rows.append( ( 'share name: ', self._name ) )
		rows.append( ( 'share text: ', self._text ) )

		gridbox = WrapInGrid( self, rows )

		timeout_box = wx.BoxSizer( wx.HORIZONTAL )
		timeout_box.Add( self._timeout_number, CC.FLAGS_EXPAND_BOTH_WAYS )
		timeout_box.Add( self._timeout_multiplier, CC.FLAGS_EXPAND_BOTH_WAYS )

		link_box = wx.BoxSizer( wx.HORIZONTAL )
		link_box.Add( self._copy_internal_share_link, CC.FLAGS_VCENTER )
		link_box.Add( self._copy_external_share_link, CC.FLAGS_VCENTER )

		b_box = wx.BoxSizer( wx.HORIZONTAL )
		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		intro = 'Sharing ' + ToHumanInt( len( self._hashes ) ) + ' files.'
		intro += os.linesep + 'Title and text are optional.'

		if new_share:
			intro += os.linesep + 'The link will not work until you ok this dialog.'

		vbox.Add( BetterStaticText( self, intro ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( timeout_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( link_box, CC.FLAGS_BUTTON_SIZER )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 350 )

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._ok.SetFocus )

	def EventCopyExternalShareURL( self, event ):
		self._service = HG.client_controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )

		external_ip = GetExternalIP() # eventually check for optional host replacement here

		external_port = self._service.GetUPnPPort()

		if external_port is None:
			external_port = self._service.GetPort()

		url = 'http://' + external_ip + ':' + str( external_port ) + '/gallery?share_key=' + self._share_key.hex()

		HG.client_controller.pub( 'clipboard', 'text', url )

	def EventCopyInternalShareURL( self, event ):
		self._service = HG.client_controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )

		internal_ip = '127.0.0.1'

		internal_port = self._service.GetPort()

		url = 'http://' + internal_ip + ':' + str( internal_port ) + '/gallery?share_key=' + self._share_key.hex()

		HG.client_controller.pub( 'clipboard', 'text', url )

	def GetInfo( self ):
		name = self._name.GetValue()

		text = self._text.GetValue()

		timeout = self._timeout_number.GetValue()

		if timeout is not None:
			timeout = timeout * self._timeout_multiplier.GetChoice() + GetNow()

		return ( self._share_key, name, text, timeout, self._hashes )

class FrameInputLocalFiles( wx.Frame ):
	def __init__( self, parent, paths = None ):

		( pos_x, pos_y ) = parent.GetPosition()

		pos = ( pos_x + 50, pos_y + 50 )

		style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT

		wx.Frame.__init__( self, parent, title = 'importing files', style = style, pos = pos )

		self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )

		self.SetIcon( HG.client_controller.frame_icon )

		if paths is None:
			paths = []

		self.SetDropTarget( FileDropTarget( self, filenames_callable = self._AddPathsToList ) )

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( '#', 4 ), ( 'path', -1 ), ( 'guessed mime', 16 ), ( 'size', 10 ) ]

		self._paths_list = BetterListCtrl( listctrl_panel, 'input_local_files', 12, 36, columns, self._ConvertListCtrlDataToTuple, delete_key_callback = self.RemovePaths )

		listctrl_panel.SetListCtrl( self._paths_list )

		listctrl_panel.AddButton( 'add files', self.AddPaths )
		listctrl_panel.AddButton( 'add folder', self.AddFolder )
		listctrl_panel.AddButton( 'remove files', self.RemovePaths, enabled_only_on_selection = True )

		self._progress = TextAndGauge( self )

		self._progress_pause = BetterBitmapButton( self, CC.GlobalBMPs.pause, self.PauseProgress )
		self._progress_pause.Disable()

		self._progress_cancel = BetterBitmapButton( self, CC.GlobalBMPs.stop, self.StopProgress )
		self._progress_cancel.Disable()

		file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'loud' )
		show_downloader_options = False

		self._file_import_options = FileImportOptionsButton( self, file_import_options, show_downloader_options )

		menu_items = []

		check_manager = CheckboxManagerOptions( 'do_human_sort_on_hdd_file_import_paths' )

		menu_items.append( ( 'check', 'sort paths as they are added', 'If checked, paths will be sorted in a numerically human-friendly (e.g. "page 9.jpg" comes before "page 10.jpg") way.', check_manager ) )

		self._cog_button = MenuBitmapButton( self, CC.GlobalBMPs.cog, menu_items )

		self._delete_after_success_st = BetterStaticText( self, style = wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE )
		self._delete_after_success_st.SetForegroundColour( ( 127, 0, 0 ) )

		self._delete_after_success = wx.CheckBox( self, label = 'delete original files after successful import' )
		self._delete_after_success.Bind( wx.EVT_CHECKBOX, self.EventDeleteAfterSuccessCheck )

		self._add_button = wx.Button( self, label = 'import now' )
		self._add_button.Bind( wx.EVT_BUTTON, self.EventOK )
		self._add_button.SetForegroundColour( ( 0, 128, 0 ) )

		self._tag_button = wx.Button( self, label = 'add tags based on filename' )
		self._tag_button.Bind( wx.EVT_BUTTON, self.EventTags )
		self._tag_button.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
		self._cancel.Bind( wx.EVT_BUTTON, self.EventCancel )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		gauge_sizer = wx.BoxSizer( wx.HORIZONTAL )

		gauge_sizer.Add( self._progress, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
		gauge_sizer.Add( self._progress_pause, CC.FLAGS_VCENTER )
		gauge_sizer.Add( self._progress_cancel, CC.FLAGS_VCENTER )

		delete_hbox = wx.BoxSizer( wx.HORIZONTAL )

		delete_hbox.Add( self._delete_after_success_st, CC.FLAGS_EXPAND_BOTH_WAYS )
		delete_hbox.Add( self._delete_after_success, CC.FLAGS_VCENTER )

		import_options_buttons = wx.BoxSizer( wx.HORIZONTAL )

		import_options_buttons.Add( self._file_import_options, CC.FLAGS_SIZER_VCENTER )
		import_options_buttons.Add( self._cog_button, CC.FLAGS_SIZER_VCENTER )

		buttons = wx.BoxSizer( wx.HORIZONTAL )

		buttons.Add( self._add_button, CC.FLAGS_VCENTER )
		buttons.Add( self._tag_button, CC.FLAGS_VCENTER )
		buttons.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( gauge_sizer, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( delete_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( import_options_buttons, CC.FLAGS_LONE_BUTTON )
		vbox.Add( buttons, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		if x < 780:
			x = 780
		if y < 480:
			y = 480

		self.SetInitialSize( ( x, y ) )

		self._lock = threading.Lock()

		self._current_path_data = {}

		self._job_key = JobKey()

		self._unparsed_paths_queue = queue.Queue()
		self._currently_parsing = threading.Event()
		self._work_to_do = threading.Event()
		self._parsed_path_queue = queue.Queue()
		self._pause_event = threading.Event()
		self._cancel_event = threading.Event()

		self._progress_updater = ThreadToGUIUpdater( self._progress, self._progress.SetValue )

		if len( paths ) > 0:
			self._AddPathsToList( paths )

		self.Show()

		HG.client_controller.gui.RegisterUIUpdateWindow( self )

		HG.client_controller.CallToThreadLongRunning( self.THREADParseImportablePaths, self._unparsed_paths_queue, self._currently_parsing, self._work_to_do, self._parsed_path_queue, self._progress_updater, self._pause_event, self._cancel_event )

	def _AddPathsToList( self, paths ):
		if self._cancel_event.is_set():
			message = 'Please wait for the cancel to clear.'

			wx.MessageBox( message )

			return

		self._unparsed_paths_queue.put( paths )

	def _ConvertListCtrlDataToTuple( self, path ):
		( index, mime, size ) = self._current_path_data[ path ]

		pretty_index = ToHumanInt( index )
		pretty_mime = HC.mime_string_lookup[ mime ]
		pretty_size = ToHumanBytes( size )

		display_tuple = ( pretty_index, path, pretty_mime, pretty_size )
		sort_tuple = ( index, path, pretty_mime, size )

		return ( display_tuple, sort_tuple )

	def _TidyUp( self ):
		self._pause_event.set()

	def AddFolder( self ):
		with wx.DirDialog( self, 'Select a folder to add.', style = wx.DD_DIR_MUST_EXIST ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				path = dlg.GetPath()

				self._AddPathsToList( ( path, ) )

	def AddPaths( self ):
		with wx.FileDialog( self, 'Select the files to add.', style = wx.FD_MULTIPLE ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				paths = dlg.GetPaths()

				self._AddPathsToList( paths )

	def EventCancel( self, event ):
		self._TidyUp()

		self.Close()

	def EventDeleteAfterSuccessCheck( self, event ):
		if self._delete_after_success.GetValue():
			self._delete_after_success_st.SetLabelText( 'YOUR ORIGINAL FILES WILL BE DELETED' )

		else:
			self._delete_after_success_st.SetLabelText( '' )

	def EventOK( self, event ):
		self._TidyUp()

		paths = self._paths_list.GetData()

		if len( paths ) > 0:
			file_import_options = self._file_import_options.GetValue()

			paths_to_service_keys_to_tags = collections.defaultdict( ServiceKeysToTags )

			delete_after_success = self._delete_after_success.GetValue()

			HG.client_controller.pub( 'new_hdd_import', paths, file_import_options, paths_to_service_keys_to_tags, delete_after_success )

		self.Close()

	def EventTags( self, event ):
		paths = self._paths_list.GetData()

		if len( paths ) > 0:
			file_import_options = self._file_import_options.GetValue()

			with DialogEdit( self, 'filename tagging', frame_key = 'local_import_filename_tagging' ) as dlg:
				panel = EditLocalImportFilenameTaggingPanel( dlg, paths )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					paths_to_service_keys_to_tags = panel.GetValue()

					delete_after_success = self._delete_after_success.GetValue()

					HG.client_controller.pub( 'new_hdd_import', paths, file_import_options, paths_to_service_keys_to_tags, delete_after_success )

					self.Close()

	def PauseProgress( self ):
		if self._pause_event.is_set():
			self._pause_event.clear()

		else:
			self._pause_event.set()

	def StopProgress( self ):
		self._cancel_event.set()

	def RemovePaths( self ):
		with DialogYesNo( self, 'Remove all selected?' ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				paths_to_delete = self._paths_list.GetData( only_selected = True )

				self._paths_list.DeleteSelected()

				for path in paths_to_delete:
					del self._current_path_data[ path ]

				flat_path_data = [ ( index, path, mime, size ) for ( path, ( index, mime, size ) ) in self._current_path_data.items() ]

				flat_path_data.sort()

				new_index = 1

				for ( old_index, path, mime, size ) in flat_path_data:
					self._current_path_data[ path ] = ( new_index, mime, size )

					new_index += 1

				self._paths_list.UpdateDatas()

	def THREADParseImportablePaths( self, unparsed_paths_queue, currently_parsing, work_to_do, parsed_path_queue, progress_updater, pause_event, cancel_event ):
		unparsed_paths = collections.deque()

		num_files_done = 0
		num_good_files = 0

		num_empty_files = 0
		num_unimportable_mime_files = 0
		num_occupied_files = 0

		while not HG.view_shutdown:
			if not self:
				return

			if len( unparsed_paths ) > 0:
				work_to_do.set()

			else:
				work_to_do.clear()

			currently_parsing.clear()

			# ready to start, let's update ui on status

			total_paths = num_files_done + len( unparsed_paths )

			if num_good_files == 0:
				if num_files_done == 0:
					message = 'waiting for paths to parse'

				else:
					message = 'none of the ' + ToHumanInt( total_paths ) + ' files parsed successfully'

			else:
				message = ConvertValueRangeToPrettyString( num_good_files, total_paths ) + ' files parsed successfully'

			if num_empty_files > 0 or num_unimportable_mime_files > 0 or num_occupied_files > 0:
				if num_good_files == 0:
					message += ': '

				else:
					message += ', but '

				bad_comments = []

				if num_empty_files > 0:
					bad_comments.append( ToHumanInt( num_empty_files ) + ' were empty' )

				if num_unimportable_mime_files > 0:
					bad_comments.append( ToHumanInt( num_unimportable_mime_files ) + ' had unsupported file types' )

				if num_occupied_files > 0:
					bad_comments.append( ToHumanInt( num_occupied_files ) + ' were inaccessible (maybe in use by another process)' )

				message += ' and '.join( bad_comments )

			message += '.'

			progress_updater.Update( message, num_files_done, total_paths )

			# status updated, lets see what work there is to do

			if cancel_event.is_set():
				while not unparsed_paths_queue.empty():
					try:
						unparsed_paths_queue.get( block = False )

					except queue.Empty:
						pass

				unparsed_paths = collections.deque()

				cancel_event.clear()
				pause_event.clear()

				continue

			if pause_event.is_set():
				time.sleep( 1 )

				continue

			# let's see if there is anything to parse

			# first we'll flesh out unparsed_paths with anything new to look at

			do_human_sort = HG.client_controller.new_options.GetBoolean( 'do_human_sort_on_hdd_file_import_paths' )

			while not unparsed_paths_queue.empty():
				try:
					raw_paths = unparsed_paths_queue.get( block = False )

					paths = GetAllPaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths

					unparsed_paths.extend( paths )

				except queue.Empty:
					pass

			# if unparsed_paths still has nothing, we'll nonetheless sleep on new paths

			if len( unparsed_paths ) == 0:
				try:
					raw_paths = unparsed_paths_queue.get( timeout = 5 )

					paths = GetAllPaths( raw_paths, do_human_sort = do_human_sort ) # convert any dirs to subpaths

					unparsed_paths.extend( paths )

				except queue.Empty:
					pass

				continue # either we added some or didn't--in any case, restart the cycle

			path = unparsed_paths.popleft()

			currently_parsing.set()

			# we are now dealing with a file. let's clear out quick no-gos

			num_files_done += 1

			if path.endswith( os.path.sep + 'Thumbs.db' ) or path.endswith( os.path.sep + 'thumbs.db' ):
				num_unimportable_mime_files += 1

				continue

			if not PathIsFree( path ):
				num_occupied_files += 1

				continue

			size = os.path.getsize( path )

			if size == 0:
				Print( 'Empty file: ' + path )

				num_empty_files += 1

				continue

			# looks good, let's burn some CPU

			mime = GetMime( path )

			if mime in HC.ALLOWED_MIMES:
				num_good_files += 1

				parsed_path_queue.put( ( path, mime, size ) )

			else:
				Print( 'Unparsable file: ' + path )

				num_unimportable_mime_files += 1

	def TIMERUIUpdate( self ):
		good_paths = list()

		while not self._parsed_path_queue.empty():
			( path, mime, size ) = self._parsed_path_queue.get()

			if not self._paths_list.HasData( path ):
				good_paths.append( path )

				index = len( self._current_path_data ) + 1

				self._current_path_data[ path ] = ( index, mime, size )

		if len( good_paths ) > 0:
			self._paths_list.AddDatas( good_paths )

		#

		paused = self._pause_event.is_set()
		no_work_in_queue = not self._work_to_do.is_set()
		working_on_a_file_now = self._currently_parsing.is_set()

		can_import = ( no_work_in_queue or paused ) and not working_on_a_file_now

		if no_work_in_queue:
			self._progress_pause.Disable()
			self._progress_cancel.Disable()

		else:
			self._progress_pause.Enable()
			self._progress_cancel.Enable()

		if can_import:
			self._add_button.Enable()
			self._tag_button.Enable()

		else:
			self._add_button.Disable()
			self._tag_button.Disable()

		if paused:
			SetBitmapButtonBitmap( self._progress_pause, CC.GlobalBMPs.play )

		else:
			SetBitmapButtonBitmap( self._progress_pause, CC.GlobalBMPs.pause )

class DialogInputNamespaceRegex( Dialog ):
	def __init__( self, parent, namespace = '', regex = '' ):

		Dialog.__init__( self, parent, 'configure quick namespace' )

		self._namespace = wx.TextCtrl( self )

		self._regex = wx.TextCtrl( self )

		self._shortcuts = RegexButton( self )

		self._regex_intro_link = BetterHyperLink( self, 'a good regex introduction', 'http://www.aivosto.com/vbtips/regex.html' )
		self._regex_practise_link = BetterHyperLink( self, 'regex practise', 'http://regexr.com/3cvmf' )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'OK' )
		self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._namespace.SetValue( namespace )
		self._regex.SetValue( regex )

		#

		control_box = wx.BoxSizer( wx.HORIZONTAL )

		control_box.Add( self._namespace, CC.FLAGS_EXPAND_BOTH_WAYS )
		control_box.Add( BetterStaticText( self, ':' ), CC.FLAGS_VCENTER )
		control_box.Add( self._regex, CC.FLAGS_EXPAND_BOTH_WAYS )

		b_box = wx.BoxSizer( wx.HORIZONTAL )
		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		intro = r'Put the namespace (e.g. page) on the left.' + os.linesep + r'Put the regex (e.g. [1-9]+\d*( ?=.{4}$ ) ) on the right.' + os.linesep + r'All files will be tagged with "namespace:regex".'

		vbox.Add( BetterStaticText( self, intro ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( control_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._shortcuts, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._regex_intro_link, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._regex_practise_link, CC.FLAGS_LONE_BUTTON )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._ok.SetFocus )

	def EventOK( self, event ):
		( namespace, regex ) = self.GetInfo()

		if namespace == '':
			wx.MessageBox( 'Please enter something for the namespace.' )

			return

		try:
			re.compile( regex )

		except Exception as e:
			text = 'That regex would not compile!'
			text += os.linesep * 2
			text += str( e )

			wx.MessageBox( text )

			return

		self.EndModal( wx.ID_OK )

	def GetInfo( self ):
		namespace = self._namespace.GetValue()

		regex = self._regex.GetValue()

		return ( namespace, regex )

class DialogInputNewFormField( Dialog ):
	def __init__( self, parent, form_field = None ):

		Dialog.__init__( self, parent, 'configure form field' )

		if form_field is None:
			( name, field_type, default, editable ) = ( '', CC.FIELD_TEXT, '', True )

		else:
			( name, field_type, default, editable ) = form_field

		self._name = wx.TextCtrl( self )

		self._type = wx.Choice( self )

		self._default = wx.TextCtrl( self )

		self._editable = wx.CheckBox( self )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'OK' )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._name.SetValue( name )

		for temp_type in CC.FIELDS:
			self._type.Append( CC.field_string_lookup[ temp_type ], temp_type )
		self._type.Select( field_type )

		self._default.SetValue( default )

		self._editable.SetValue( editable )

		#

		rows = []

		rows.append( ( 'name: ', self._name ) )
		rows.append( ( 'type: ', self._type ) )
		rows.append( ( 'default: ', self._default ) )
		rows.append( ( 'editable: ', self._editable ) )

		gridbox = WrapInGrid( self, rows )

		b_box = wx.BoxSizer( wx.HORIZONTAL )

		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._ok.SetFocus )

	def GetFormField( self ):
		name = self._name.GetValue()

		field_type = self._type.GetClientData( self._type.GetSelection() )

		default = self._default.GetValue()

		editable = self._editable.GetValue()

		return ( name, field_type, default, editable )

class DialogInputTags( Dialog ):
	def __init__( self, parent, service_key, tags, message = '' ):
		Dialog.__init__( self, parent, 'input tags' )

		self._service_key = service_key

		self._tags = ListBoxTagsStringsAddRemove( self, service_key = service_key )

		expand_parents = True

		self._tag_box = AutoCompleteDropdownTagsWrite( self, self.EnterTags, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key, null_entry_callable = self.OK )

		self._ok = wx.Button( self, id= wx.ID_OK, label = 'OK' )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._tags.SetTags( tags )

		#

		b_box = wx.BoxSizer( wx.HORIZONTAL )

		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		if message != '':
			vbox.Add( BetterStaticText( self, message ), CC.FLAGS_EXPAND_PERPENDICULAR )

		vbox.Add( self._tags, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( self._tag_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 300 )

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._tag_box.SetFocus )

	def EnterTags( self, tags ):
		tag_parents_manager = HG.client_controller.tag_parents_manager

		parents = set()

		for tag in tags:
			some_parents = tag_parents_manager.GetParents( self._service_key, tag )

			parents.update( some_parents )

		if len( tags ) > 0:
			self._tags.EnterTags( tags )
			self._tags.AddTags( parents )

	def GetTags( self ):
		return self._tags.GetTags()

	def OK( self ):
		self.EndModal( wx.ID_OK )

class DialogInputUPnPMapping( Dialog ):
	def __init__( self, parent, external_port, protocol_type, internal_port, description, duration ):
		Dialog.__init__( self, parent, 'configure upnp mapping' )

		self._external_port = wx.SpinCtrl( self, min = 0, max = 65535 )

		self._protocol_type = BetterChoice( self )
		self._protocol_type.Append( 'TCP', 'TCP' )
		self._protocol_type.Append( 'UDP', 'UDP' )

		self._internal_port = wx.SpinCtrl( self, min = 0, max = 65535 )
		self._description = wx.TextCtrl( self )
		self._duration = wx.SpinCtrl( self, min = 0, max = 86400 )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'OK' )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'Cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._external_port.SetValue( external_port )

		if protocol_type == 'TCP':
			self._protocol_type.Select( 0 )
		elif protocol_type == 'UDP':
			self._protocol_type.Select( 1 )

		self._internal_port.SetValue( internal_port )
		self._description.SetValue( description )
		self._duration.SetValue( duration )

		#

		rows = []

		rows.append( ( 'external port: ', self._external_port ) )
		rows.append( ( 'protocol type: ', self._protocol_type ) )
		rows.append( ( 'internal port: ', self._internal_port ) )
		rows.append( ( 'description: ', self._description ) )
		rows.append( ( 'duration (0 = indefinite ): ', self._duration ) )

		gridbox = WrapInGrid( self, rows )

		b_box = wx.BoxSizer( wx.HORIZONTAL )
		b_box.Add( self._ok, CC.FLAGS_VCENTER )
		b_box.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
		vbox.Add( b_box, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._ok.SetFocus )

	def GetInfo( self ):
		external_port = self._external_port.GetValue()
		protocol_type = self._protocol_type.GetChoice()
		internal_port = self._internal_port.GetValue()
		description = self._description.GetValue()
		duration = self._duration.GetValue()

		return ( external_port, protocol_type, internal_port, description, duration )

class DialogModifyAccounts( Dialog ):
	def __init__( self, parent, service_key, subject_identifiers ):
		Dialog.__init__( self, parent, 'modify account' )

		self._service = HG.client_controller.services_manager.GetService( service_key )
		self._subject_identifiers = list( subject_identifiers )

		#

		self._account_info_panel = StaticBox( self, 'account info' )

		self._subject_text = wx.StaticText( self._account_info_panel )

		#

		self._account_types_panel = StaticBox( self, 'account types' )

		self._account_types = wx.Choice( self._account_types_panel )

		self._account_types_ok = wx.Button( self._account_types_panel, label = 'OK' )
		self._account_types_ok.Bind( wx.EVT_BUTTON, self.EventChangeAccountType )

		#

		self._expiration_panel = StaticBox( self, 'change expiration' )

		self._add_to_expires = wx.Choice( self._expiration_panel )

		self._add_to_expires_ok = wx.Button( self._expiration_panel, label = 'OK' )
		self._add_to_expires_ok.Bind( wx.EVT_BUTTON, self.EventAddToExpires )

		self._set_expires = wx.Choice( self._expiration_panel )

		self._set_expires_ok = wx.Button( self._expiration_panel, label = 'OK' )
		self._set_expires_ok.Bind( wx.EVT_BUTTON, self.EventSetExpires )

		#

		self._ban_panel = StaticBox( self, 'bans' )

		self._ban = wx.Button( self._ban_panel, label = 'ban user' )
		self._ban.Bind( wx.EVT_BUTTON, self.EventBan )
		self._ban.SetBackgroundColour( ( 255, 0, 0 ) )
		self._ban.SetForegroundColour( ( 255, 255, 0 ) )

		self._superban = wx.Button( self._ban_panel, label = 'ban user and delete every contribution they have ever made' )
		self._superban.Bind( wx.EVT_BUTTON, self.EventSuperban )
		self._superban.SetBackgroundColour( ( 255, 0, 0 ) )
		self._superban.SetForegroundColour( ( 255, 255, 0 ) )

		self._exit = wx.Button( self, id = wx.ID_CANCEL, label = 'Exit' )

		#

		if len( self._subject_identifiers ) == 1:
			( subject_identifier, ) = self._subject_identifiers

			response = self._service.Request( HC.GET, 'account_info', { 'subject_identifier' : subject_identifier } )

			subject_string = str( response[ 'account_info' ] )

		else:
			subject_string = 'modifying ' + ToHumanInt( len( self._subject_identifiers ) ) + ' accounts'

		self._subject_text.SetLabelText( subject_string )

		#

		response = self._service.Request( HC.GET, 'account_types' )

		account_types = response[ 'account_types' ]

		for account_type in account_types:
			self._account_types.Append( account_type.ConvertToString(), account_type )

		self._account_types.SetSelection( 0 )

		#

		for ( _string, value ) in HC.lifetimes:
			if value is not None:
				self._add_to_expires.Append( _string, value ) # don't want 'add no limit'

		self._add_to_expires.SetSelection( 1 ) # three months

		for ( _string, value ) in HC.lifetimes:
			self._set_expires.Append( _string, value )
			self._set_expires.SetSelection( 1 ) # three months

		#

		self._account_info_panel.Add( self._subject_text, CC.FLAGS_EXPAND_PERPENDICULAR )

		account_types_hbox = wx.BoxSizer( wx.HORIZONTAL )

		account_types_hbox.Add( self._account_types, CC.FLAGS_VCENTER )
		account_types_hbox.Add( self._account_types_ok, CC.FLAGS_VCENTER )

		self._account_types_panel.Add( account_types_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )

		add_to_expires_box = wx.BoxSizer( wx.HORIZONTAL )

		add_to_expires_box.Add( wx.StaticText( self._expiration_panel, label = 'add to expires: ' ), CC.FLAGS_VCENTER )
		add_to_expires_box.Add( self._add_to_expires, CC.FLAGS_EXPAND_BOTH_WAYS )
		add_to_expires_box.Add( self._add_to_expires_ok, CC.FLAGS_VCENTER )

		set_expires_box = wx.BoxSizer( wx.HORIZONTAL )

		set_expires_box.Add( wx.StaticText( self._expiration_panel, label = 'set expires to: ' ), CC.FLAGS_VCENTER )
		set_expires_box.Add( self._set_expires, CC.FLAGS_EXPAND_BOTH_WAYS )
		set_expires_box.Add( self._set_expires_ok, CC.FLAGS_VCENTER )

		self._expiration_panel.Add( add_to_expires_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		self._expiration_panel.Add( set_expires_box, CC.FLAGS_EXPAND_PERPENDICULAR )

		self._ban_panel.Add( self._ban, CC.FLAGS_EXPAND_PERPENDICULAR )
		self._ban_panel.Add( self._superban, CC.FLAGS_EXPAND_PERPENDICULAR )

		vbox = wx.BoxSizer( wx.VERTICAL )
		vbox.Add( self._account_info_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._account_types_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._expiration_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._ban_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._exit, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._exit.SetFocus )

	def _DoModification( self ):
		# change this to saveaccounts or whatever. the previous func changes the accounts, and then we push that change
		# generate accounts, with the modification having occurred

		self._service.Request( HC.POST, 'account', { 'accounts' : self._accounts } )

		if len( self._subject_identifiers ) == 1:
			( subject_identifier, ) = self._subject_identifiers

			response = self._service.Request( HC.GET, 'account_info', { 'subject_identifier' : subject_identifier } )

			account_info = response[ 'account_info' ]

			self._subject_text.SetLabelText( str( account_info ) )

		if len( self._subject_identifiers ) > 1:
			wx.MessageBox( 'Done!' )

	def EventAddToExpires( self, event ):
		raise NotImplementedError()
		#self._DoModification( HC.ADD_TO_EXPIRES, timespan = self._add_to_expires.GetClientData( self._add_to_expires.GetSelection() ) )

	def EventBan( self, event ):
		with DialogTextEntry( self, 'Enter reason for the ban.' ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				raise NotImplementedError()
				#self._DoModification( HC.BAN, reason = dlg.GetValue() )

	def EventChangeAccountType( self, event ):
		raise NotImplementedError()
		#self._DoModification( HC.CHANGE_ACCOUNT_TYPE, account_type_key = self._account_types.GetChoice() )

	def EventSetExpires( self, event ):
		expires = self._set_expires.GetClientData( self._set_expires.GetSelection() )

		if expires is not None:
			expires += GetNow()

		raise NotImplementedError()
		#self._DoModification( HC.SET_EXPIRES, expires = expires )

	def EventSuperban( self, event ):
		with DialogTextEntry( self, 'Enter reason for the superban.' ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				raise NotImplementedError()
				#self._DoModification( HC.SUPERBAN, reason = dlg.GetValue() )

class DialogSelectFromURLTree( Dialog ):
	def __init__( self, parent, url_tree ):
		Dialog.__init__( self, parent, 'select items' )

		agwStyle = wx.lib.agw.customtreectrl.TR_DEFAULT_STYLE | wx.lib.agw.customtreectrl.TR_AUTO_CHECK_CHILD

		self._tree = wx.lib.agw.customtreectrl.CustomTreeCtrl( self, agwStyle = agwStyle )

		self._ok = wx.Button( self, id = wx.ID_OK )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		( text_gumpf, name, size, children ) = url_tree

		root_name = self._RenderItemName( name, size )

		root_item = self._tree.AddRoot( root_name, ct_type = 1 )

		self._AddDirectory( root_item, children )

		self._tree.CheckItem( root_item )

		self._tree.Expand( root_item )

		#

		button_hbox = wx.BoxSizer( wx.HORIZONTAL )

		button_hbox.Add( self._ok, CC.FLAGS_VCENTER )
		button_hbox.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._tree, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( button_hbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 640 )
		y = max( y, 640 )

		self.SetInitialSize( ( x, y ) )

	def _AddDirectory( self, root, children ):
		for ( child_type, name, size, data ) in children:
			item_name = self._RenderItemName( name, size )

			if child_type == 'file':
				self._tree.AppendItem( root, item_name, ct_type = 1, data = data )

			else:
				subroot = self._tree.AppendItem( root, item_name, ct_type = 1 )

				self._AddDirectory( subroot, data )

	def _GetSelectedChildrenData( self, parent_item ):
		result = []

		cookie = 0

		( child_item, cookie ) = self._tree.GetNextChild( parent_item, cookie )

		while child_item is not None:
			data = self._tree.GetItemPyData( child_item )

			if data is None:
				result.extend( self._GetSelectedChildrenData( child_item ) )

			else:
				if self._tree.IsItemChecked( child_item ):
					result.append( data )

			( child_item, cookie ) = self._tree.GetNextChild( parent_item, cookie )

		return result

	def _RenderItemName( self, name, size ):
		return name + ' - ' + ToHumanBytes( size )

	def GetURLs( self ):
		root_item = self._tree.GetRootItem()

		urls = self._GetSelectedChildrenData( root_item )

		return urls

class DialogSelectImageboard( Dialog ):
	def __init__( self, parent ):
		Dialog.__init__( self, parent, 'select imageboard' )

		self._tree = wx.TreeCtrl( self )
		self._tree.Bind( wx.EVT_TREE_ITEM_ACTIVATED, self.EventActivate )

		#

		all_imageboards = HG.client_controller.Read( 'imageboards' )

		root_item = self._tree.AddRoot( 'all sites' )

		for ( site, imageboards ) in list( all_imageboards.items() ):
			site_item = self._tree.AppendItem( root_item, site )

			for imageboard in imageboards:
				name = imageboard.GetName()

				self._tree.AppendItem( site_item, name, data = wx.TreeItemData( imageboard ) )

		self._tree.Expand( root_item )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._tree, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		if x < 320:
			x = 320
		if y < 640:
			y = 640

		self.SetInitialSize( ( x, y ) )

	def EventActivate( self, event ):
		item = self._tree.GetSelection()

		data_object = self._tree.GetItemData( item )

		if data_object is None:
			self._tree.Toggle( item )
		else:
			self.EndModal( wx.ID_OK )

	def GetImageboard( self ): return self._tree.GetItemData( self._tree.GetSelection() ).GetData()

class DialogTextEntry( Dialog ):
	def __init__( self, parent, message, default = '', allow_blank = False, suggestions = None, max_chars = None ):
		if suggestions is None:
			suggestions = []

		Dialog.__init__( self, parent, 'enter text', position = 'center' )

		self._chosen_suggestion = None
		self._allow_blank = allow_blank
		self._max_chars = max_chars

		button_choices = []

		for text in suggestions:
			button_choices.append( BetterButton( self, text, self.ButtonChoice, text ) )

		self._text = wx.TextCtrl( self, style = wx.TE_PROCESS_ENTER )
		self._text.Bind( wx.EVT_TEXT, self.EventText )
		self._text.Bind( wx.EVT_TEXT_ENTER, self.EventEnter )

		if self._max_chars is not None:
			self._text.SetMaxLength( self._max_chars )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'ok' )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		self._text.SetValue( default )

		self._CheckText()

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._ok, CC.FLAGS_SMALL_INDENT )
		hbox.Add( self._cancel, CC.FLAGS_SMALL_INDENT )

		st_message = BetterStaticText( self, message )
		st_message.SetWrapWidth( 480 )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( st_message, CC.FLAGS_BIG_INDENT )

		for button in button_choices:
			vbox.Add( button, CC.FLAGS_EXPAND_PERPENDICULAR )

		vbox.Add( self._text, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( hbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 250 )

		self.SetInitialSize( ( x, y ) )

	def _CheckText( self ):
		if not self._allow_blank:
			if self._text.GetValue() == '':
				self._ok.Disable()

			else:
				self._ok.Enable()

	def ButtonChoice( self, text ):
		self._chosen_suggestion = text

		self.EndModal( wx.ID_OK )

	def EventText( self, event ):
		wx.CallAfter( self._CheckText )

		event.Skip()

	def EventEnter( self, event ):
		if self._text.GetValue() != '':
			self.EndModal( wx.ID_OK )

	def GetValue( self ):
		if self._chosen_suggestion is None:
			return self._text.GetValue()

		else:
			return self._chosen_suggestion

class DialogYesNo( Dialog ):
	def __init__( self, parent, message, title = 'Are you sure?', yes_label = 'yes', no_label = 'no' ):
		Dialog.__init__( self, parent, title, position = 'center' )

		self._yes = wx.Button( self, id = wx.ID_YES )
		self._yes.SetForegroundColour( ( 0, 128, 0 ) )
		self._yes.SetLabelText( yes_label )

		self._no = wx.Button( self, id = wx.ID_NO )
		self._no.SetForegroundColour( ( 128, 0, 0 ) )
		self._no.SetLabelText( no_label )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._yes, CC.FLAGS_SMALL_INDENT )
		hbox.Add( self._no, CC.FLAGS_SMALL_INDENT )

		vbox = wx.BoxSizer( wx.VERTICAL )

		text = BetterStaticText( self, message )

		text.SetWrapWidth( 480 )

		vbox.Add( text, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( hbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 250 )

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( self._yes.SetFocus )

class DialogYesYesNo( Dialog ):
	def __init__( self, parent, message, title = 'Are you sure?', yes_tuples = None, no_label = 'no' ):
		if yes_tuples is None:
			yes_tuples = [ ( 'yes', 'yes' ) ]

		Dialog.__init__( self, parent, title, position = 'center' )

		self._value = None

		yes_buttons = []

		for ( label, data ) in yes_tuples:
			yes_button = BetterButton( self, label, self._DoYes, data )
			yes_button.SetForegroundColour( ( 0, 128, 0 ) )

			yes_buttons.append( yes_button )

		self._no = wx.Button( self, id = wx.ID_NO )
		self._no.SetForegroundColour( ( 128, 0, 0 ) )
		self._no.SetLabelText( no_label )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		for yes_button in yes_buttons:
			hbox.Add( yes_button, CC.FLAGS_SMALL_INDENT )

		hbox.Add( self._no, CC.FLAGS_SMALL_INDENT )

		vbox = wx.BoxSizer( wx.VERTICAL )

		text = BetterStaticText( self, message )

		text.SetWrapWidth( 480 )

		vbox.Add( text, CC.FLAGS_BIG_INDENT )
		vbox.Add( hbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 250 )

		self.SetInitialSize( ( x, y ) )

		wx.CallAfter( yes_buttons[0].SetFocus )

	def _DoYes( self, value ):
		self._value = value

		self.EndModal( wx.ID_YES )

	def GetValue( self ):
		return self._value

license: wtfpl