code:
import collections, itertools, wx, wx.lib.scrolledpanel

from lib.GUI.Common import BetterBitmapButton, BetterButton, ClientToScreen, \
	IsWXAncestor, OnOffButton, WindowOrAnyTLPChildHasFocus
from lib.GUI.ListBoxes import ListBoxTagsACRead, ListBoxTagsACWrite
from lib.GUI.Menus import AppendMenuItem
from lib.GUI.Shortcuts import ConvertKeyEventToSimpleTuple

from lib.Hydrus.Data import GetNowFloat, TimeHasPassedFloat
from lib.Hydrus.Data import GetLighterDarkerColour, MergePredicates
from lib.Hydrus.Exceptions import SizeException, WXDeadWindowException
from lib.Hydrus.Search import ConvertEntryTextToSearchText, FilterPredicatesBySearchText, \
	FilterTagsBySearchText, IsComplexWildcard, Predicate, SortPredicates
from lib.Hydrus.Tags import CheckTagNotEmpty, CleanTag, SplitTag
from lib.Hydrus.Threading import JobKey

### CONSTANTS ###

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

ID_TIMER_DROPDOWN_HIDE = wx.NewId()
ID_TIMER_AC_LAG = wx.NewId()

( SelectUpEvent, EVT_SELECT_UP ) = wx.lib.newevent.NewCommandEvent()
( SelectDownEvent, EVT_SELECT_DOWN ) = wx.lib.newevent.NewCommandEvent()

( ShowPreviousEvent, EVT_SHOW_PREVIOUS ) = wx.lib.newevent.NewCommandEvent()
( ShowNextEvent, EVT_SHOW_NEXT ) = wx.lib.newevent.NewCommandEvent()

### FUNCTIONS ###

def ReadFetch( win, job_key, results_callable, parsed_search_text, wx_media_callable, file_search_context, synchronised, include_unusual_predicate_types, initial_matches_fetched, search_text_for_current_cache, cached_results, under_construction_or_predicate ):
	next_search_is_probably_fast = False

	include_current = file_search_context.IncludeCurrentTags()
	include_pending = file_search_context.IncludePendingTags()

	file_service_key = file_search_context.GetFileServiceKey()
	tag_service_key = file_search_context.GetTagServiceKey()

	num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]

	( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = parsed_search_text

	if search_text in ( '', ':', '*' ):
		# if the user inputs '-' or similar, let's go to an empty list
		if raw_entry == '':
			input_just_changed = search_text_for_current_cache is not None

			db_not_going_to_hang_if_we_hit_it = not HG.client_controller.DBCurrentlyDoingJob()

			if input_just_changed or db_not_going_to_hang_if_we_hit_it or not initial_matches_fetched:
				if file_service_key == CC.COMBINED_FILE_SERVICE_KEY:
					search_service_key = tag_service_key

				else:
					search_service_key = file_service_key

				search_text_for_current_cache = None

				cached_results = HG.client_controller.Read( 'file_system_predicates', search_service_key )

			matches = cached_results

		else:
			matches = []

	else:
		( namespace, half_complete_subtag ) = SplitTag( search_text )

		siblings_manager = HG.client_controller.tag_siblings_manager

		if False and half_complete_subtag == '':
			search_text_for_current_cache = None

			matches = [] # a query like 'namespace:'

		else:
			fetch_from_db = True

			if synchronised and wx_media_callable is not None:
				try:
					media = HG.client_controller.CallBlockingToWX( win, wx_media_callable )

				except WXDeadWindowException:
					return

				can_fetch_from_media = media is not None and len( media ) > 0

				if can_fetch_from_media:
					fetch_from_db = False

			if fetch_from_db:
				# if user searches 'blah', then we include 'blah (23)' for 'series:blah (10)', 'blah (13)'
				# if they search for 'series:blah', then we don't!
				add_namespaceless = ':' not in namespace

				small_and_specific_search = cache_text is not None and len( cache_text ) < num_autocomplete_chars

				if small_and_specific_search:
					predicates = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = cache_text, exact_match = True, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = add_namespaceless, job_key = job_key, collapse_siblings = True )

				else:
					cache_invalid_for_this_search = cache_text is None or search_text_for_current_cache is None or not cache_text.startswith( search_text_for_current_cache )

					if cache_invalid_for_this_search:
						# new_search_text_for_current_cache = cache_text

						cached_results = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = search_text, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = add_namespaceless, job_key = job_key, collapse_siblings = True )

					predicates = cached_results

					next_search_is_probably_fast = True

			else:
				# it is possible that media will change between calls to this, so don't cache it
				# it's also quick as hell, so who cares

				tags_managers = []

				for m in media:
					if m.IsCollection():
						tags_managers.extend( m.GetSingletonsTagsManagers() )

					else:
						tags_managers.append( m.GetTagsManager() )

				tags_to_do = set()

				current_tags_to_count = collections.Counter()
				pending_tags_to_count = collections.Counter()

				if include_current:
					lists_of_current_tags = [ list( tags_manager.GetCurrent( tag_service_key ) ) for tags_manager in tags_managers ]

					current_tags_flat_iterable = itertools.chain.from_iterable( lists_of_current_tags )

					current_tags_flat = FilterTagsBySearchText( tag_service_key, search_text, current_tags_flat_iterable )

					current_tags_to_count.update( current_tags_flat )

					tags_to_do.update( list( current_tags_to_count.keys() ) )

				if include_pending:
					lists_of_pending_tags = [ list( tags_manager.GetPending( tag_service_key ) ) for tags_manager in tags_managers ]

					pending_tags_flat_iterable = itertools.chain.from_iterable( lists_of_pending_tags )

					pending_tags_flat = FilterTagsBySearchText( tag_service_key, search_text, pending_tags_flat_iterable )

					pending_tags_to_count.update( pending_tags_flat )

					tags_to_do.update( list( pending_tags_to_count.keys() ) )

				predicates = [ Predicate( HC.PREDICATE_TYPE_TAG, tag, inclusive, current_tags_to_count[ tag ], pending_tags_to_count[ tag ] ) for tag in tags_to_do ]

				if tag_service_key != CC.COMBINED_TAG_SERVICE_KEY:
					predicates = siblings_manager.CollapsePredicates( tag_service_key, predicates )

				if namespace == '':
					predicates = MergePredicates( predicates, add_namespaceless = True )

				next_search_is_probably_fast = True

			matches = FilterPredicatesBySearchText( tag_service_key, search_text, predicates )

			matches = SortPredicates( matches )

		if include_unusual_predicate_types:
			if explicit_wildcard:
				matches.insert( 0, Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive ) )

			else:
				if namespace != '' and half_complete_subtag in ( '', '*' ):
					matches.insert( 0, Predicate( HC.PREDICATE_TYPE_NAMESPACE, namespace, inclusive ) )

		for match in matches:
			if match.GetInclusive() != inclusive:
				match.SetInclusive( inclusive )

		try:
			index = matches.index( entry_predicate )

			predicate = matches[ index ]

			del matches[ index ]

			matches.insert( 0, predicate )

		except Exception:
			pass

	if under_construction_or_predicate is not None:
		matches.insert( 0, under_construction_or_predicate )

	if job_key.IsCancelled():
		return

	HG.client_controller.CallLaterWXSafe( win, 0.0, results_callable, job_key, search_text, search_text_for_current_cache, cached_results, matches, next_search_is_probably_fast )

def PutAtTopOfMatches( matches, predicate ):
	try:
		index = matches.index( predicate )

		predicate = matches[ index ]

		matches.remove( predicate )

	except ValueError:
		pass

	matches.insert( 0, predicate )

def WriteFetch( win, job_key, results_callable, parsed_search_text, file_service_key, tag_service_key, expand_parents, search_text_for_current_cache, cached_results ):
	next_search_is_probably_fast = False

	num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]

	( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = parsed_search_text

	if search_text in ( '', ':', '*' ):
		search_text_for_current_cache = None

		matches = []

	else:
		must_do_a_search = False

		small_and_specific_search = cache_text is not None and len( cache_text ) < num_autocomplete_chars

		if small_and_specific_search:
			predicates = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = cache_text, exact_match = True, add_namespaceless = False, job_key = job_key, collapse_siblings = False )

		else:
			cache_invalid_for_this_search = cache_text is None or search_text_for_current_cache is None or not cache_text.startswith( search_text_for_current_cache )

			if must_do_a_search or cache_invalid_for_this_search:
				search_text_for_current_cache = cache_text

				cached_results = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = search_text, add_namespaceless = False, job_key = job_key, collapse_siblings = False )

			predicates = cached_results

			next_search_is_probably_fast = True

		matches = FilterPredicatesBySearchText( tag_service_key, search_text, predicates )

		matches = SortPredicates( matches )

		PutAtTopOfMatches( matches, entry_predicate )

		if sibling_predicate is not None:
			PutAtTopOfMatches( matches, sibling_predicate )

		if expand_parents:
			parents_manager = HG.client_controller.tag_parents_manager

			matches = parents_manager.ExpandPredicates( tag_service_key, matches )

	HG.client_controller.CallLaterWXSafe( win, 0.0, results_callable, job_key, search_text, search_text_for_current_cache, cached_results, matches, next_search_is_probably_fast )

# much of this is based on the excellent TexCtrlAutoComplete class by Edward Flick, Michele Petrazzo and Will Sadkin, just with plenty of simplification and integration into hydrus

class AutoCompleteDropdown( wx.Panel ):
	def __init__( self, parent ):
		wx.Panel.__init__( self, parent )

		self._intercept_key_events = True

		tlp = self.GetTopLevelParent()

		# There's a big bug in wx where FRAME_FLOAT_ON_PARENT Frames don't get passed their mouse events if their parent is a Dialog jej
		# I think it is something to do with the initialisation order; if the frame is init'ed before the ShowModal call, but whatever.

		# This turned out to be ugly when I added the manage tags frame, so I've set it to if the tlp has a parent, which basically means "not the main gui"

		not_main_gui = tlp.GetParent() is not None

		if not_main_gui or HC.options[ 'always_embed_autocompletes' ] or not HC.PLATFORM_WINDOWS:
			self._float_mode = False

		else:
			self._float_mode = True

		self._text_ctrl = wx.TextCtrl( self, style=wx.TE_PROCESS_ENTER )

		self._UpdateBackgroundColour()

		self._last_attempted_dropdown_width = 0
		self._last_attempted_dropdown_position = ( None, None )

		self._last_move_event_started = 0.0
		self._last_move_event_occurred = 0.0

		if self._float_mode:
			self._text_ctrl.Bind( wx.EVT_SET_FOCUS, self.EventSetFocus )
			self._text_ctrl.Bind( wx.EVT_KILL_FOCUS, self.EventKillFocus )

		self._text_ctrl.Bind( wx.EVT_TEXT, self.EventText )
		self._text_ctrl.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )

		self._text_ctrl.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._text_ctrl, CC.FLAGS_EXPAND_PERPENDICULAR )

		#self._dropdown_window = wx.PopupWindow( self, flags = wx.BORDER_RAISED )
		#self._dropdown_window = wx.PopupTransientWindow( self, style = wx.BORDER_RAISED )
		#self._dropdown_window = wx.Window( self, style = wx.BORDER_RAISED )

		#self._dropdown_window = wx.Panel( self )

		if self._float_mode:
			self._dropdown_window = wx.Frame( self, style = wx.FRAME_TOOL_WINDOW | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.BORDER_RAISED )

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

			self._dropdown_window.SetPosition( ClientToScreen( self._text_ctrl, ( 0, 0 ) ) )

			self._dropdown_window.Bind( wx.EVT_CLOSE, self.EventCloseDropdown )

			self._dropdown_hidden = True

			self._list_height_num_chars = 19

		else:
			self._dropdown_window = wx.Panel( self )

			self._list_height_num_chars = 8

		self._dropdown_notebook = wx.Notebook( self._dropdown_window )

		#

		self._search_results_list = self._InitSearchResultsList()

		self._dropdown_notebook.AddPage( self._search_results_list, 'results', True )

		#

		if not self._float_mode:
			vbox.Add( self._dropdown_window, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

		self._last_fetched_search_text = ''
		self._next_search_is_probably_fast = False

		self._search_text_for_current_cache = None
		self._cached_results = []

		self._current_fetch_job_key = None

		self._initial_matches_fetched = False

		self._move_hide_job = None
		self._refresh_list_job = None

		if self._float_mode:
			self.Bind( wx.EVT_MOVE, self.EventMove )
			self.Bind( wx.EVT_SIZE, self.EventMove )

			HG.client_controller.sub( self, '_ParentMovedOrResized', 'main_gui_move_event' )

			parent = self

			while True:
				try:
					parent = parent.GetParent()

					if isinstance( parent, wx.ScrolledWindow ):
						parent.Bind( wx.EVT_SCROLLWIN, self.EventMove )

				except Exception:
					break

		HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
		HG.client_controller.sub( self, 'DoDropdownHideShow', 'notify_page_change' )

		self._ScheduleListRefresh( 0.0 )

	def _BroadcastChoices( self, predicates, shift_down ):
		raise NotImplementedError()

	def _BroadcastCurrentText( self, shift_down ):
		text = self._text_ctrl.GetValue()

		self._BroadcastChoices( { text }, shift_down )

	def _CancelScheduledListRefresh( self ):
		if self._refresh_list_job is not None:
			self._refresh_list_job.Cancel()

	def _ClearInput( self ):
		self._text_ctrl.SetValue( '' )

		self._SetResultsToList( [] )

		self._ScheduleListRefresh( 0.0 )

	def _DropdownHideShow( self ):
		if not self._float_mode:
			return

		try:
			if self._ShouldShow():
				self._ShowDropdown()

				if self._move_hide_job is not None:
					self._move_hide_job.Cancel()

					self._move_hide_job = None

			else:
				self._HideDropdown()

		except Exception:
			if self._move_hide_job is not None:
				self._move_hide_job.Cancel()

				self._move_hide_job = None

			raise

	def _HandleEscape( self ):
		if self._float_mode:
			self.GetTopLevelParent().SetFocus()

			return True

		else:
			return False

	def _HideDropdown( self ):
		if not self._dropdown_hidden:
			self._dropdown_window.Hide()

			self._dropdown_hidden = True

	def _InitSearchResultsList( self ):
		raise NotImplementedError()

	def _ParentMovedOrResized( self ):
		if self._float_mode:
			if TimeHasPassedFloat( self._last_move_event_occurred + 1.0 ):
				self._last_move_event_started = GetNowFloat()

			self._last_move_event_occurred = GetNowFloat()

			# we'll do smoother move updates for a little bit to stop flickeryness, but after that we'll just hide

			NICE_ANIMATION_GRACE_PERIOD = 0.25

			time_to_delay_these_calls = TimeHasPassedFloat( self._last_move_event_started + NICE_ANIMATION_GRACE_PERIOD )

			if time_to_delay_these_calls:
				self._HideDropdown()

				if self._ShouldShow():
					if self._move_hide_job is None:
						self._move_hide_job = HG.client_controller.CallRepeatingWXSafe( self._dropdown_window, 0.0, 0.25, self._DropdownHideShow )

					self._move_hide_job.Delay( 0.25 )

			else:
				self._DropdownHideShow()

	def _ScheduleListRefresh( self, delay ):
		if self._refresh_list_job is not None and delay == 0.0:
			self._refresh_list_job.Wake()

		else:
			self._CancelScheduledListRefresh()

			self._refresh_list_job = HG.client_controller.CallLaterWXSafe( self, delay, self._UpdateSearchResultsList )

	def _SetListDirty( self ):
		self._search_text_for_current_cache = None

		self._ScheduleListRefresh( 0.0 )

	def _SetResultsToList( self, results ):
		raise NotImplementedError()

	def _ShouldShow( self ):
		tlp_active = self.GetTopLevelParent().IsActive() or self._dropdown_window.IsActive()

		if HC.PLATFORM_LINUX:
			tlp = self.GetTopLevelParent()

			if isinstance( tlp, wx.Dialog ):
				visible = True

			else:
				# notebook on linux doesn't 'hide' things apparently, so isshownonscreen, which recursively tests parents' hide status, doesn't work!

				gui = HG.client_controller.GetGUI()

				current_page = gui.GetCurrentPage()

				visible = IsWXAncestor( self, current_page )

		else:
			visible = self._text_ctrl.IsShownOnScreen()

		focus_remains_on_self_or_children = WindowOrAnyTLPChildHasFocus( self )

		return tlp_active and visible and focus_remains_on_self_or_children

	def _ShouldTakeResponsibilityForEnter( self ):
		raise NotImplementedError()

	def _ShowDropdown( self ):
		( text_width, text_height ) = self._text_ctrl.GetSize()

		if self._text_ctrl.IsShown():
			desired_dropdown_position = ClientToScreen( self._text_ctrl, ( -2, text_height - 2 ) )

			if self._last_attempted_dropdown_position != desired_dropdown_position:
				self._dropdown_window.SetPosition( desired_dropdown_position )

				self._last_attempted_dropdown_position = desired_dropdown_position

		#

		# show_and_fit_needed = False

		if self._dropdown_hidden:
			self._dropdown_window.Show()

			self._dropdown_hidden = False

		if text_width != self._last_attempted_dropdown_width:
			# show_and_fit_needed = True

			self._dropdown_window.Fit()

			self._dropdown_window.SetSize( ( text_width, -1 ) )

			self._dropdown_window.Layout()

			self._last_attempted_dropdown_width = text_width

	def _StartResultsFetchJob( self, job_key ):
		raise NotImplementedError()

	def _TakeResponsibilityForEnter( self, shift_down ):
		raise NotImplementedError()

	def _UpdateBackgroundColour( self ):
		colour = HG.client_controller.new_options.GetColour( CC.COLOUR_AUTOCOMPLETE_BACKGROUND )

		if not self._intercept_key_events:
			colour = GetLighterDarkerColour( colour )

		self._text_ctrl.SetBackgroundColour( colour )

		self._text_ctrl.Refresh()

	def _UpdateSearchResultsList( self ):
		self._refresh_list_job = None

		if self._current_fetch_job_key is not None:
			self._current_fetch_job_key.Cancel()

		self._current_fetch_job_key = JobKey( cancellable = True )

		self._StartResultsFetchJob( self._current_fetch_job_key )

	def BroadcastChoices( self, predicates, shift_down = False ):
		self._BroadcastChoices( predicates, shift_down )

	def DoDropdownHideShow( self ):
		self._DropdownHideShow()

	def EventCharHook( self, event ):
		HG.client_controller.ResetIdleTimer()

		( modifier, key ) = ConvertKeyEventToSimpleTuple( event )

		if key in ( wx.WXK_INSERT, wx.WXK_NUMPAD_INSERT ):
			self._intercept_key_events = not self._intercept_key_events

			self._UpdateBackgroundColour()

		elif key == wx.WXK_SPACE and event.RawControlDown(): # this is control, not command on os x, for which command+space does some os stuff

			self._ScheduleListRefresh( 0.0 )

		elif self._intercept_key_events:
			send_input_to_current_list = False

			current_results_list = self._dropdown_notebook.GetCurrentPage()

			current_list_is_empty = len( current_results_list ) == 0

			input_is_empty = self._text_ctrl.GetValue() == ''

			if key in ( ord( 'A' ), ord( 'a' ) ) and modifier == wx.ACCEL_CTRL:
				event.Skip()

			elif key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ) and self._ShouldTakeResponsibilityForEnter():
				shift_down = modifier == wx.ACCEL_SHIFT

				self._TakeResponsibilityForEnter( shift_down )

			elif input_is_empty: # maybe we should be sending a 'move' event to a different place

				if key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ) and current_list_is_empty:
					if key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ):
						new_event = SelectUpEvent( -1 )

					elif key in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ):
						new_event = SelectDownEvent( -1 )

					wx.QueueEvent( self.GetEventHandler(), new_event )

				elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and current_list_is_empty:
					if key in ( wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ):
						new_event = ShowPreviousEvent( -1 )

					elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN ):
						new_event = ShowNextEvent( -1 )

					wx.QueueEvent( self.GetEventHandler(), new_event )

				elif key in ( wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT, wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT ):
					if key in ( wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT ):
						direction = -1

					elif key in ( wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT ):
						direction = 1

					self.MoveNotebookPageFocus( direction = direction )

				elif key == wx.WXK_ESCAPE:
					escape_caught = self._HandleEscape()

					if not escape_caught:
						send_input_to_current_list = True

				else:
					send_input_to_current_list = True

			else:
				send_input_to_current_list = True

			if send_input_to_current_list:
				# Don't say QueueEvent here--it duplicates the event processing at higher levels, leading to 2 x F9, for instance
				current_results_list.EventCharHook( event ) # ultimately, this typically skips the event, letting the text ctrl take it

		else:
			event.Skip()

	def EventCloseDropdown( self, event ):
		HG.client_controller.GetGUI().Close()

	def EventKillFocus( self, event ):
		if self._float_mode:
			self._DropdownHideShow()

		event.Skip()

	def EventMouseWheel( self, event ):
		current_results_list = self._dropdown_notebook.GetCurrentPage()

		if self._text_ctrl.GetValue() == '' and len( current_results_list ) == 0:
			if event.GetWheelRotation() > 0:
				new_event = SelectUpEvent( -1 )

			else:
				new_event = SelectDownEvent( -1 )

			wx.QueueEvent( self.GetEventHandler(), new_event )

		else:
			if event.CmdDown():
				if event.GetWheelRotation() > 0:
					current_results_list.MoveSelectionUp()

				else:
					current_results_list.MoveSelectionDown()

			else:
				# for some reason, the scrolledwindow list doesn't process scroll events properly when in a popupwindow
				# so let's just tell it to scroll manually

				( start_x, start_y ) = current_results_list.GetViewStart()

				if event.GetWheelRotation() > 0:
					current_results_list.Scroll( -1, start_y - 3 )

				else:
					current_results_list.Scroll( -1, start_y + 3 )

				if event.GetWheelRotation() > 0:
					command_type = wx.wxEVT_SCROLLWIN_LINEUP

				else:
					command_type = wx.wxEVT_SCROLLWIN_LINEDOWN

				wx.QueueEvent( current_results_list.GetEventHandler(), wx.ScrollWinEvent( command_type ) )

	def EventMove( self, event ):
		self._ParentMovedOrResized()

		event.Skip()

	def EventSetFocus( self, event ):
		if self._float_mode:
			self._DropdownHideShow()

		event.Skip()

	def EventText( self, event ):
		num_chars = len( self._text_ctrl.GetValue() )

		if num_chars == 0:
			self._ScheduleListRefresh( 0.0 )

		else:
			if HC.options[ 'fetch_ac_results_automatically' ]:
				( char_limit, long_wait, short_wait ) = HC.options[ 'ac_timings' ]

				self._next_search_is_probably_fast = self._next_search_is_probably_fast and num_chars > len( self._last_fetched_search_text )

				if self._next_search_is_probably_fast:
					self._ScheduleListRefresh( 0.0 )

				elif num_chars < char_limit:
					self._ScheduleListRefresh( long_wait / 1000.0 )

				else:
					self._ScheduleListRefresh( short_wait / 1000.0 )

			if self._dropdown_notebook.GetCurrentPage() != self._search_results_list:
				self.MoveNotebookPageFocus( index = 0 )

	def ForceSizeCalcNow( self ):
		if self._float_mode:
			self._DropdownHideShow()

	def MoveNotebookPageFocus( self, index = None, direction = None ):
		new_index = None

		if index is not None:
			new_index = index

		elif direction is not None:
			current_index = self._dropdown_notebook.GetSelection()

			if current_index is not None and current_index != wx.NOT_FOUND:
				number_of_pages = self._dropdown_notebook.GetPageCount()

				new_index = ( current_index + direction ) % number_of_pages # does wraparound

		if new_index is not None:
			self._dropdown_notebook.ChangeSelection( new_index )

			self._text_ctrl.SetFocus()

	def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results ):
		if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
			self._last_fetched_search_text = search_text

			self._search_text_for_current_cache = search_text_for_cache
			self._cached_results = cached_results

			self._current_fetch_job_key = None

			self._initial_matches_fetched = True

			self._SetResultsToList( results )

	def SetFocus( self ):
		if HC.PLATFORM_OSX:
			wx.CallAfter( self._text_ctrl.SetFocus )

		else:
			self._text_ctrl.SetFocus()

class AutoCompleteDropdownTags( AutoCompleteDropdown ):
	def __init__( self, parent, file_service_key, tag_service_key ):
		self._file_service_key = file_service_key
		self._tag_service_key = tag_service_key

		AutoCompleteDropdown.__init__( self, parent )

		self._allow_all_known_files = True

		file_service = HG.client_controller.services_manager.GetService( self._file_service_key )

		tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )

		self._file_repo_button = BetterButton( self._dropdown_window, file_service.GetName(), self.FileButtonHit )
		self._file_repo_button.SetMinSize( ( 20, -1 ) )

		self._tag_repo_button = BetterButton( self._dropdown_window, tag_service.GetName(), self.TagButtonHit )
		self._tag_repo_button.SetMinSize( ( 20, -1 ) )

		self._favourites_list = self._InitFavouritesList()

		self.RefreshFavouriteTags()

		self._dropdown_notebook.AddPage( self._favourites_list, 'favourites', False )

		#

		HG.client_controller.sub( self, 'RefreshFavouriteTags', 'notify_new_favourite_tags' )

	def _ChangeFileService( self, file_service_key ):
		if file_service_key == CC.COMBINED_FILE_SERVICE_KEY and self._tag_service_key == CC.COMBINED_TAG_SERVICE_KEY:
			self._ChangeTagService( CC.LOCAL_TAG_SERVICE_KEY )

		self._file_service_key = file_service_key

		file_service = HG.client_controller.services_manager.GetService( self._file_service_key )

		name = file_service.GetName()

		self._file_repo_button.SetLabelText( name )

		self._SetListDirty()

	def _ChangeTagService( self, tag_service_key ):
		if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY and self._file_service_key == CC.COMBINED_FILE_SERVICE_KEY:
			self._ChangeFileService( CC.LOCAL_FILE_SERVICE_KEY )

		self._tag_service_key = tag_service_key

		self._search_results_list.SetTagService( self._tag_service_key )

		tag_service = tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )

		name = tag_service.GetName()

		self._tag_repo_button.SetLabelText( name )

		self._search_text_for_current_cache = None

		self._SetListDirty()

	def _InitFavouritesList( self ):
		raise NotImplementedError()

	def _SetResultsToList( self, results ):
		self._search_results_list.SetPredicates( results )

	def FileButtonHit( self ):
		services_manager = HG.client_controller.services_manager

		services = []

		services.append( services_manager.GetService( CC.LOCAL_FILE_SERVICE_KEY ) )
		services.append( services_manager.GetService( CC.TRASH_SERVICE_KEY ) )
		services.append( services_manager.GetService( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
		services.extend( services_manager.GetServices( ( HC.FILE_REPOSITORY, ) ) )

		advanced_mode = HG.client_controller.new_options.GetBoolean( 'advanced_mode' )

		if advanced_mode and self._allow_all_known_files:
			services.append( services_manager.GetService( CC.COMBINED_FILE_SERVICE_KEY ) )

		menu = wx.Menu()

		for service in services:
			AppendMenuItem( self, menu, service.GetName(), 'Change the current file domain to ' + service.GetName() + '.', self._ChangeFileService, service.GetServiceKey() )

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

	def RefreshFavouriteTags( self ):
		favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )

		favourite_tags.sort()

		predicates = [ Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ]

		self._favourites_list.SetPredicates( predicates )

	def SetFileService( self, file_service_key ):
		self._ChangeFileService( file_service_key )

	def SetTagService( self, tag_service_key ):
		self._ChangeTagService( tag_service_key )

	def TagButtonHit( self ):
		services_manager = HG.client_controller.services_manager

		services = []

		services.append( services_manager.GetService( CC.LOCAL_TAG_SERVICE_KEY ) )
		services.extend( services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) )
		services.append( services_manager.GetService( CC.COMBINED_TAG_SERVICE_KEY ) )

		menu = wx.Menu()

		for service in services:
			AppendMenuItem( self, menu, service.GetName(), 'Change the current tag domain to ' + service.GetName() + '.', self._ChangeTagService, service.GetServiceKey() )

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

class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
	def __init__( self, parent, page_key, file_search_context, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True ):
		file_service_key = file_search_context.GetFileServiceKey()
		tag_service_key = file_search_context.GetTagServiceKey()

		AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )

		self._allow_all_known_files = allow_all_known_files

		self._media_callable = media_callable
		self._page_key = page_key

		self._under_construction_or_predicate = None

		self._file_search_context = file_search_context

		self._include_current_tags = OnOffButton( self._dropdown_window, self._page_key, 'notify_include_current', on_label = 'include current tags', off_label = 'exclude current tags', start_on = file_search_context.IncludeCurrentTags() )
		self._include_current_tags.SetToolTip( 'select whether to include current tags in the search' )
		self._include_pending_tags = OnOffButton( self._dropdown_window, self._page_key, 'notify_include_pending', on_label = 'include pending tags', off_label = 'exclude pending tags', start_on = file_search_context.IncludePendingTags() )
		self._include_pending_tags.SetToolTip( 'select whether to include pending tags in the search' )

		self._synchronised = OnOffButton( self._dropdown_window, self._page_key, 'notify_search_immediately', on_label = 'searching immediately', off_label = 'waiting -- tag counts may be inaccurate', start_on = synchronised )
		self._synchronised.SetToolTip( 'select whether to renew the search as soon as a new predicate is entered' )

		self._or_cancel = BetterBitmapButton( self._dropdown_window, CC.GlobalBMPs.delete, self._CancelORConstruction )
		self._or_cancel.SetToolTip( 'Cancel OR Predicate construction.' )
		self._or_cancel.Hide()

		self._or_rewind = BetterBitmapButton( self._dropdown_window, CC.GlobalBMPs.previous, self._RewindORConstruction )
		self._or_rewind.SetToolTip( 'Rewind OR Predicate construction.' )
		self._or_rewind.Hide()

		self._include_unusual_predicate_types = include_unusual_predicate_types

		button_hbox_1 = wx.BoxSizer( wx.HORIZONTAL )

		button_hbox_1.Add( self._include_current_tags, CC.FLAGS_EXPAND_BOTH_WAYS )
		button_hbox_1.Add( self._include_pending_tags, CC.FLAGS_EXPAND_BOTH_WAYS )

		sync_button_hbox = wx.BoxSizer( wx.HORIZONTAL )

		sync_button_hbox.Add( self._synchronised, CC.FLAGS_EXPAND_BOTH_WAYS )
		sync_button_hbox.Add( self._or_cancel, CC.FLAGS_VCENTER )
		sync_button_hbox.Add( self._or_rewind, CC.FLAGS_VCENTER )

		button_hbox_2 = wx.BoxSizer( wx.HORIZONTAL )

		button_hbox_2.Add( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
		button_hbox_2.Add( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( button_hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( sync_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( button_hbox_2, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._dropdown_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )

		self._dropdown_window.SetSizer( vbox )

		HG.client_controller.sub( self, 'SetSynchronisedWait', 'synchronised_wait_switch' )

		HG.client_controller.sub( self, 'IncludeCurrent', 'notify_include_current' )
		HG.client_controller.sub( self, 'IncludePending', 'notify_include_pending' )

	def _BroadcastChoices( self, predicates, shift_down ):
		or_pred_in_broadcast = self._under_construction_or_predicate is not None and self._under_construction_or_predicate in predicates

		if shift_down:
			if self._under_construction_or_predicate is None:
				self._under_construction_or_predicate = Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, predicates )

			else:
				if or_pred_in_broadcast:
					predicates.remove( self._under_construction_or_predicate )

				or_preds = list( self._under_construction_or_predicate.GetValue() )

				or_preds.extend( [ predicate for predicate in predicates if predicate not in or_preds ] )

				self._under_construction_or_predicate = Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds )

		else:
			if self._under_construction_or_predicate is not None and not or_pred_in_broadcast:
				or_preds = list( self._under_construction_or_predicate.GetValue() )

				or_preds.extend( [ predicate for predicate in predicates if predicate not in or_preds ] )

				predicates = { Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds ) }

			if or_pred_in_broadcast:
				or_preds = list( self._under_construction_or_predicate.GetValue() )

				if len( or_preds ) == 1:
					predicates.remove( self._under_construction_or_predicate )

					predicates.extend( or_preds )

			self._under_construction_or_predicate = None

			HG.client_controller.pub( 'enter_predicates', self._page_key, predicates )

		self._UpdateORButtons()

		self._ClearInput()

	def _BroadcastCurrentText( self, shift_down ):
		( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = self._ParseSearchText()

		try:
			CheckTagNotEmpty( search_text )

		except SizeException:
			return

		self._BroadcastChoices( { entry_predicate }, shift_down )

	def _CancelORConstruction( self ):
		self._under_construction_or_predicate = None

		self._UpdateORButtons()

		self._ClearInput()

	def _ChangeFileService( self, file_service_key ):
		AutoCompleteDropdownTags._ChangeFileService( self, file_service_key )

		self._file_search_context.SetFileServiceKey( file_service_key )

		HG.client_controller.pub( 'change_file_service', self._page_key, file_service_key )

		HG.client_controller.pub( 'refresh_query', self._page_key )

	def _ChangeTagService( self, tag_service_key ):
		AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )

		self._file_search_context.SetTagServiceKey( tag_service_key )

		HG.client_controller.pub( 'change_tag_service', self._page_key, tag_service_key )

		HG.client_controller.pub( 'refresh_query', self._page_key )

	def _HandleEscape( self ):
		if self._under_construction_or_predicate is not None and self._text_ctrl.GetValue() == '':
			self._CancelORConstruction()

			return True

		else:
			return AutoCompleteDropdown._HandleEscape( self )

	def _InitFavouritesList( self ):
		favs_list = ListBoxTagsACRead( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )

		return favs_list

	def _InitSearchResultsList( self ):
		return ListBoxTagsACRead( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )

	def _ParseSearchText( self ):
		raw_entry = self._text_ctrl.GetValue()

		if raw_entry.startswith( '-' ):
			inclusive = False

			entry_text = raw_entry[1:]

		else:
			inclusive = True

			entry_text = raw_entry

		tag = CleanTag( entry_text )

		explicit_wildcard = '*' in entry_text

		search_text = ConvertEntryTextToSearchText( entry_text )

		if explicit_wildcard:
			cache_text = None

			entry_predicate = Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive )

		else:
			cache_text = search_text[:-1] # take off the trailing '*' for the cache text

			siblings_manager = HG.client_controller.tag_siblings_manager

			sibling = siblings_manager.GetSibling( self._tag_service_key, tag )

			if sibling is None:
				entry_predicate = Predicate( HC.PREDICATE_TYPE_TAG, tag, inclusive )

			else:
				entry_predicate = Predicate( HC.PREDICATE_TYPE_TAG, sibling, inclusive )

		return ( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate )

	def _RewindORConstruction( self ):
		if self._under_construction_or_predicate is not None:
			or_preds = self._under_construction_or_predicate.GetValue()

			if len( or_preds ) <= 1:
				self._CancelORConstruction()

				return

			or_preds = or_preds[:-1]

			self._under_construction_or_predicate = Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds )

		self._UpdateORButtons()

		self._ClearInput()

	def _StartResultsFetchJob( self, job_key ):
		parsed_search_text = self._ParseSearchText()

		HG.client_controller.CallToThread( ReadFetch, self, job_key, self.SetFetchedResults, parsed_search_text, self._media_callable, self._file_search_context, self._synchronised.IsOn(), self._include_unusual_predicate_types, self._initial_matches_fetched, self._search_text_for_current_cache, self._cached_results, self._under_construction_or_predicate )

	def _ShouldTakeResponsibilityForEnter( self ):
		( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = self._ParseSearchText()

		sitting_on_empty = raw_entry == ''

		# when the user has quickly typed something in and the results are not yet in

		p1 = not sitting_on_empty and self._last_fetched_search_text != search_text

		return p1

	def _TakeResponsibilityForEnter( self, shift_down ):
		self._BroadcastCurrentText( shift_down )

	def _UpdateORButtons( self ):
		layout_needed = False

		if self._under_construction_or_predicate is None:
			if self._or_cancel.IsShown():
				self._or_cancel.Hide()

				layout_needed = True

			if self._or_rewind.IsShown():
				self._or_rewind.Hide()

				layout_needed = True

		else:
			or_preds = self._under_construction_or_predicate.GetValue()

			if len( or_preds ) > 1:
				if not self._or_rewind.IsShown():
					self._or_rewind.Show()

					layout_needed = True

			else:
				if self._or_rewind.IsShown():
					self._or_rewind.Hide()

					layout_needed = True

			if not self._or_cancel.IsShown():
				self._or_cancel.Show()

				layout_needed = True

		if layout_needed:
			self._dropdown_window.Layout()

	def GetFileSearchContext( self ):
		return self._file_search_context

	def IncludeCurrent( self, page_key, value ):
		if page_key == self._page_key:
			self._file_search_context.SetIncludeCurrentTags( value )

			self._SetListDirty()

			HG.client_controller.pub( 'refresh_query', self._page_key )

	def IncludePending( self, page_key, value ):
		if page_key == self._page_key:
			self._file_search_context.SetIncludePendingTags( value )

			self._SetListDirty()

			HG.client_controller.pub( 'refresh_query', self._page_key )

	def IsSynchronised( self ):
		return self._synchronised.IsOn()

	def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results, next_search_is_probably_fast ):
		if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
			AutoCompleteDropdownTags.SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results )

			self._next_search_is_probably_fast = next_search_is_probably_fast

			num_chars = len( self._text_ctrl.GetValue() )

			if num_chars == 0:
				# refresh system preds after five mins

				self._ScheduleListRefresh( 300 )

	def SetSynchronisedWait( self, page_key ):
		if page_key == self._page_key:
			self._synchronised.EventButton( None )

class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
	def __init__( self, parent, chosen_tag_callable, expand_parents, file_service_key, tag_service_key, null_entry_callable = None, tag_service_key_changed_callable = None ):
		self._chosen_tag_callable = chosen_tag_callable
		self._expand_parents = expand_parents
		self._null_entry_callable = null_entry_callable
		self._tag_service_key_changed_callable = tag_service_key_changed_callable

		if tag_service_key != CC.COMBINED_TAG_SERVICE_KEY and HC.options[ 'show_all_tags_in_autocomplete' ]:
			file_service_key = CC.COMBINED_FILE_SERVICE_KEY

		if tag_service_key == CC.LOCAL_TAG_SERVICE_KEY:
			file_service_key = CC.LOCAL_FILE_SERVICE_KEY

		AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )

		vbox = wx.BoxSizer( wx.VERTICAL )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )

		vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._dropdown_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )

		self._dropdown_window.SetSizer( vbox )

	def _BroadcastChoices( self, predicates, shift_down ):
		tags = { predicate.GetValue() for predicate in predicates }

		if len( tags ) > 0:
			self._chosen_tag_callable( tags )

		self._ClearInput()

	def _ParseSearchText( self ):
		raw_entry = self._text_ctrl.GetValue()

		tag = CleanTag( raw_entry )

		search_text = ConvertEntryTextToSearchText( raw_entry )

		if IsComplexWildcard( search_text ):
			cache_text = None

		else:
			cache_text = search_text[:-1] # take off the trailing '*' for the cache text

		entry_predicate = Predicate( HC.PREDICATE_TYPE_TAG, tag )

		siblings_manager = HG.client_controller.tag_siblings_manager

		sibling = siblings_manager.GetSibling( self._tag_service_key, tag )

		if sibling is not None:
			sibling_predicate = Predicate( HC.PREDICATE_TYPE_TAG, sibling )

		else:
			sibling_predicate = None

		return ( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate )

	def _BroadcastCurrentText( self, shift_down ):
		( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()

		try:
			CheckTagNotEmpty( search_text )

		except SizeException:
			return

		self._BroadcastChoices( { entry_predicate }, shift_down )

	def _ChangeTagService( self, tag_service_key ):
		AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )

		if self._tag_service_key_changed_callable is not None:
			self._tag_service_key_changed_callable( tag_service_key )

	def _InitFavouritesList( self ):
		favs_list = ListBoxTagsACWrite( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )

		return favs_list

	def _InitSearchResultsList( self ):
		return ListBoxTagsACWrite( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )

	def _ShouldTakeResponsibilityForEnter( self ):
		( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()

		sitting_on_empty = raw_entry == ''

		# when the user has quickly typed something in and the results are not yet in

		p1 = not sitting_on_empty and self._last_fetched_search_text != search_text

		# when the text ctrl is empty, we are looking at search results, and we want to push a None to the parent dialog

		p2 = sitting_on_empty and self._dropdown_notebook.GetCurrentPage() == self._search_results_list

		return p1 or p2

	def _StartResultsFetchJob( self, job_key ):
		parsed_search_text = self._ParseSearchText()

		HG.client_controller.CallToThread( WriteFetch, self, job_key, self.SetFetchedResults, parsed_search_text, self._file_service_key, self._tag_service_key, self._expand_parents, self._search_text_for_current_cache, self._cached_results )

	def _TakeResponsibilityForEnter( self, shift_down ):
		if self._text_ctrl.GetValue() == '' and self._dropdown_notebook.GetCurrentPage() == self._search_results_list:
			if self._null_entry_callable is not None:
				self._null_entry_callable()

		else:
			self._BroadcastCurrentText( shift_down )

	def RefreshFavouriteTags( self ):
		favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )

		favourite_tags.sort()

		predicates = [ Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ]

		parents_manager = HG.client_controller.tag_parents_manager

		predicates = parents_manager.ExpandPredicates( CC.COMBINED_TAG_SERVICE_KEY, predicates )

		self._favourites_list.SetPredicates( predicates )

	def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results, next_search_is_probably_fast ):
		if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
			AutoCompleteDropdownTags.SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results )

			self._next_search_is_probably_fast = next_search_is_probably_fast


code:
import os, wx

from lib.GUI.Common import BetterCheckListBox, BetterStaticText, \
	ConvertTextToPixelWidth, WrapInGrid
from lib.GUI.ScrolledPanels import EditPanel, ReviewPanel
from lib.GUI.Tags import TagFilterButton

from lib.Hydrus.Data import GetNow

### CONSTANTS ###

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

from lib.Hydrus.API.Local import ALLOWED_PERMISSIONS, APIPermissions, \
	BASIC_PERMISSION_TO_STR_LOOKUP, CLIENT_API_PERMISSION["SEARCH_FILES"], \
	last_api_permissions_request

### CLASSES ###
class CaptureAPIAccessPermissionsRequestPanel( ReviewPanel ):
	def __init__( self, parent ):
		ReviewPanel.__init__( self, parent )

		self._time_started = GetNow()

		self._api_permissions = None

		self._st = BetterStaticText( self, label = 'waiting for request\u2026' )

		vbox = wx.BoxSizer( wx.VERTICAL )

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

		self.SetSizer( vbox )

		self._repeating_job = HG.client_controller.CallRepeatingWXSafe( self, 0.0, 0.5, self.REPEATINGUpdate )

	def GetAPIAccessPermissions( self ):
		return self._api_permissions

	def REPEATINGUpdate( self ):
		api_permissions_request = last_api_permissions_request

		if api_permissions_request is not None:
			self._api_permissions = api_permissions_request

			wx.MessageBox( 'Got request!' )

			last_api_permissions_request = None
			self._repeating_job.Cancel()

			parent = self.GetParent()

			if parent.IsModal(): # event sometimes fires after modal done

				parent.DoOK()

class EditAPIPermissionsPanel( EditPanel ):
	def __init__( self, parent, api_permissions ):
		EditPanel.__init__( self, parent )

		self._original_api_permissions = api_permissions

		self._access_key = wx.TextCtrl( self, style = wx.TE_READONLY )

		width = ConvertTextToPixelWidth( self._access_key, 66 )

		self._access_key.SetMinClientSize( ( width, -1 ) )

		self._name = wx.TextCtrl( self )

		self._basic_permissions = BetterCheckListBox( self )

		for permission in ALLOWED_PERMISSIONS:
			self._basic_permissions.Append( BASIC_PERMISSION_TO_STR_LOOKUP[ permission ], permission )

		search_tag_filter = api_permissions.GetSearchTagFilter()

		message = 'The API will only permit searching for tags that pass through this filter.'
		message += os.linesep * 2
		message += 'If you want to allow all tags, just leave it as is, permitting everything. If you want to limit it to just one tag, such as "do waifu2x on this", set up a whitelist with only that tag allowed.'

		self._search_tag_filter = TagFilterButton( self, message, search_tag_filter )

		#

		access_key = api_permissions.GetAccessKey()

		self._access_key.SetValue( access_key.hex() )

		name = api_permissions.GetName()

		self._name.SetValue( name )

		basic_permissions = api_permissions.GetBasicPermissions()

		self._basic_permissions.SetCheckedData( basic_permissions )

		#

		rows = []

		rows.append( ( 'access key: ', self._access_key ) )
		rows.append( ( 'name: ', self._name ) )
		rows.append( ( 'permissions: ', self._basic_permissions) )
		rows.append( ( 'tag search permissions: ', self._search_tag_filter ) )

		gridbox = WrapInGrid( self, rows )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		self.SetSizer( vbox )

		#

		self._UpdateEnabled()

		self._basic_permissions.Bind( wx.EVT_CHECKLISTBOX, self.EventCheckList )

	def _UpdateEnabled( self ):
		can_search = CLIENT_API_PERMISSION["SEARCH_FILES"] in self._basic_permissions.GetChecked()

		self._search_tag_filter.Enable( can_search )

	def _GetValue( self ):
		name = self._name.GetValue()
		access_key = bytes.fromhex( self._access_key.GetValue() )

		basic_permissions = self._basic_permissions.GetChecked()
		search_tag_filter = self._search_tag_filter.GetValue()

		api_permissions = APIPermissions( name = name, access_key = access_key, basic_permissions = basic_permissions, search_tag_filter = search_tag_filter )

		return api_permissions

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

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

		return api_permissions

license: wtfpl