code:
import os, sys, traceback, wx

from lib.GUI import FrameGUI
from lib.GUI.Common import BetterBitmapButton, BetterButton, BetterStaticText, \
	ClientToScreen, ConvertTextToPixelWidth, Gauge, IsWXAncestor, SetBitmapButtonBitmap
from lib.GUI.Controls import NetworkJobControl
from lib.GUI.Dialogs import DialogYesNo
from lib.GUI.ScrolledPanels import ReviewPanel
from lib.GUI.TopLevelWindows import Frame, MouseIsOnMyDisplay, NewDialog, \
	PostSizeChangedEvent

from lib.Hydrus.Data import Print, ShowException, ToHumanInt
from lib.Hydrus.Data import CatchExceptionClient, ShowExceptionClient, \
	ShowTextClient
from lib.Hydrus.Exceptions import VetoException
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

### CLASSES ###

class PopupWindow( wx.Window ):
	def __init__( self, parent ):
		wx.Window.__init__( self, parent, style = wx.BORDER_SIMPLE )

		self.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )

	def TryToDismiss( self ):
		self.GetParent().Dismiss( self )

	def EventDismiss( self, event ):
		self.TryToDismiss()

class PopupDismissAll( PopupWindow ):
	def __init__( self, parent ):
		PopupWindow.__init__( self, parent )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		self._text = BetterStaticText( self )
		self._text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )

		button = wx.Button( self, label = 'dismiss all' )
		button.Bind( wx.EVT_BUTTON, self.EventButton )
		button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )

		hbox.Add( self._text, CC.FLAGS_VCENTER )
		hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( button, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def TryToDismiss( self ):
		pass

	def EventButton( self, event ):
		self.GetParent().DismissAll()

	def SetNumMessages( self, num_messages_pending ):
		self._text.SetLabelText( ToHumanInt( num_messages_pending ) + ' more messages' )

class PopupMessage( PopupWindow ):
	TEXT_CUTOFF = 1024

	def __init__( self, parent, job_key ):
		PopupWindow.__init__( self, parent )

		self._job_key = job_key

		vbox = wx.BoxSizer( wx.VERTICAL )

		self._title = BetterStaticText( self, style = wx.ALIGN_CENTER )

		popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )

		wrap_width = ConvertTextToPixelWidth( self._title, popup_message_character_width )

		if HG.client_controller.new_options.GetBoolean( 'popup_message_force_min_width' ):
			self.SetMinClientSize( ( wrap_width, -1 ) )

		self._title.SetWrapWidth( wrap_width )
		self._title.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._title.Hide()

		self._text_1 = BetterStaticText( self )
		self._text_1.SetWrapWidth( wrap_width )
		self._text_1.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._text_1.Hide()

		self._gauge_1 = Gauge( self, size = ( wrap_width, -1 ) )
		self._gauge_1.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._gauge_1.Hide()

		self._text_2 = BetterStaticText( self )
		self._text_2.SetWrapWidth( wrap_width )
		self._text_2.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._text_2.Hide()

		self._gauge_2 = Gauge( self, size = ( wrap_width, -1 ) )
		self._gauge_2.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._gauge_2.Hide()

		self._text_yes_no = BetterStaticText( self )
		self._text_yes_no.SetWrapWidth( wrap_width )
		self._text_yes_no.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._text_yes_no.Hide()

		self._yes = BetterButton( self, 'yes', self._YesButton )
		self._yes.Hide()

		self._no = BetterButton( self, 'no', self._NoButton )
		self._no.Hide()

		self._network_job_ctrl = NetworkJobControl( self )
		self._network_job_ctrl.Hide()

		self._copy_to_clipboard_button = BetterButton( self, 'copy to clipboard', self.CopyToClipboard )
		self._copy_to_clipboard_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._copy_to_clipboard_button.Hide()

		self._show_files_button = BetterButton( self, 'show files', self.ShowFiles )
		self._show_files_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._show_files_button.Hide()

		self._show_tb_button = BetterButton( self, 'show traceback', self.ShowTB )
		self._show_tb_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._show_tb_button.Hide()

		self._tb_text = BetterStaticText( self )
		self._tb_text.SetWrapWidth( wrap_width )
		self._tb_text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._tb_text.Hide()

		self._copy_tb_button = BetterButton( self, 'copy traceback information', self.CopyTB )
		self._copy_tb_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._copy_tb_button.Hide()

		self._pause_button = BetterBitmapButton( self, CC.GlobalBMPs.pause, self.PausePlay )
		self._pause_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._pause_button.Hide()

		self._cancel_button = BetterBitmapButton( self, CC.GlobalBMPs.stop, self.Cancel )
		self._cancel_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
		self._cancel_button.Hide()

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._pause_button, CC.FLAGS_VCENTER )
		hbox.Add( self._cancel_button, CC.FLAGS_VCENTER )

		yes_no_hbox = wx.BoxSizer( wx.HORIZONTAL )

		yes_no_hbox.Add( self._yes, CC.FLAGS_VCENTER )
		yes_no_hbox.Add( self._no, CC.FLAGS_VCENTER )

		vbox.Add( self._title, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._text_1, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._gauge_1, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._text_2, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._gauge_2, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._text_yes_no, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( yes_no_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._network_job_ctrl, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._copy_to_clipboard_button, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._show_files_button, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._show_tb_button, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._tb_text, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._copy_tb_button, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( hbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

	def _NoButton( self ):
		self._job_key.SetVariable( 'popup_yes_no_answer', False )

		self._job_key.Delete()

		self._yes.Hide()
		self._no.Hide()

	def _ProcessText( self, text ):
		text = str( text )

		if len( text ) > self.TEXT_CUTOFF:
			new_text = 'The text is too long to display here. Here is the start of it (the rest is printed to the log):'

			new_text += os.linesep * 2

			new_text += text[:self.TEXT_CUTOFF]

			text = new_text

		return text

	def _YesButton( self ):
		self._job_key.SetVariable( 'popup_yes_no_answer', True )

		self._job_key.Delete()

		self._yes.Hide()
		self._no.Hide()

	def Cancel( self ):
		self._job_key.Cancel()

		self._pause_button.Disable()
		self._cancel_button.Disable()

	def CopyTB( self ):
		HG.client_controller.pub( 'clipboard', 'text', self._job_key.ToString() )

	def CopyToClipboard( self ):
		result = self._job_key.GetIfHasVariable( 'popup_clipboard' )

		if result is not None:
			( title, text ) = result

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

	def PausePlay( self ):
		self._job_key.PausePlay()

		if self._job_key.IsPaused():
			SetBitmapButtonBitmap( self._pause_button, CC.GlobalBMPs.play )

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

	def ShowFiles( self ):
		result = self._job_key.GetIfHasVariable( 'popup_files' )

		if result is not None:
			( popup_files, popup_files_name ) = result

			HG.client_controller.pub( 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = popup_files, page_name = popup_files_name )

	def ShowTB( self ):
		if self._tb_text.IsShown():
			self._show_tb_button.SetLabelText( 'show traceback' )

			self._tb_text.Hide()

		else:
			self._show_tb_button.SetLabelText( 'hide traceback' )

			# popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )

			# wrap_width = ConvertTextToPixelWidth( self._title, popup_message_character_width )

			self._tb_text.Show()

		self.GetParent().MakeSureEverythingFits()

	def GetJobKey( self ):
		return self._job_key

	def IsDeleted( self ):
		return self._job_key.IsDeleted()

	def TryToDismiss( self ):
		if self._job_key.IsPausable() or self._job_key.IsCancellable():
			return

		else:
			PopupWindow.TryToDismiss( self )

	def UpdateMessage( self ):
		# popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )

		# wrap_width = ConvertTextToPixelWidth( self._title, popup_message_character_width )

		paused = self._job_key.IsPaused()

		title = self._job_key.GetIfHasVariable( 'popup_title' )

		if title is not None:
			self._title.Show()

			self._title.SetLabelText( title )

		else:
			self._title.Hide()

		popup_text_1 = self._job_key.GetIfHasVariable( 'popup_text_1' )

		if popup_text_1 is not None or paused:
			if paused:
				text = 'paused'

			else:
				text = popup_text_1

			self._text_1.Show()

			self._text_1.SetLabelText( text )

		else:
			self._text_1.Hide()

		popup_gauge_1 = self._job_key.GetIfHasVariable( 'popup_gauge_1' )

		if popup_gauge_1 is not None and not paused:
			( gauge_value, gauge_range ) = popup_gauge_1

			self._gauge_1.SetRange( gauge_range )
			self._gauge_1.SetValue( gauge_value )

			self._gauge_1.Show()

		else:
			self._gauge_1.Hide()

		popup_text_2 = self._job_key.GetIfHasVariable( 'popup_text_2' )

		if popup_text_2 is not None and not paused:
			text = popup_text_2

			self._text_2.SetLabelText( self._ProcessText( text ) )

			self._text_2.Show()

		else:
			self._text_2.Hide()

		popup_gauge_2 = self._job_key.GetIfHasVariable( 'popup_gauge_2' )

		if popup_gauge_2 is not None and not paused:
			( gauge_value, gauge_range ) = popup_gauge_2

			self._gauge_2.SetRange( gauge_range )
			self._gauge_2.SetValue( gauge_value )

			self._gauge_2.Show()

		else:
			self._gauge_2.Hide()

		popup_yes_no_question = self._job_key.GetIfHasVariable( 'popup_yes_no_question' )

		if popup_yes_no_question is not None and not paused:
			text = popup_yes_no_question

			self._text_yes_no.SetLabelText( self._ProcessText( text ) )

			self._text_yes_no.Show()

			self._yes.Show()
			self._no.Show()

		else:
			self._text_yes_no.Hide()
			self._yes.Hide()
			self._no.Hide()

		if self._job_key.HasVariable( 'popup_network_job' ):
			# this can be validly None, which confuses the getifhas result

			popup_network_job = self._job_key.GetIfHasVariable( 'popup_network_job' )

			self._network_job_ctrl.SetNetworkJob( popup_network_job )

			self._network_job_ctrl.Show()

		else:
			self._network_job_ctrl.ClearNetworkJob()

			self._network_job_ctrl.Hide()

		popup_clipboard = self._job_key.GetIfHasVariable( 'popup_clipboard' )

		if popup_clipboard is not None:
			( title, text ) = popup_clipboard

			if self._copy_to_clipboard_button.GetLabelText() != title:
				self._copy_to_clipboard_button.SetLabelText( title )

			self._copy_to_clipboard_button.Show()

		else:
			self._copy_to_clipboard_button.Hide()

		result = self._job_key.GetIfHasVariable( 'popup_files' )

		if result is not None:
			( popup_files, popup_files_name ) = result

			hashes = popup_files

			text = popup_files_name + ' - show ' + ToHumanInt( len( hashes ) ) + ' files'

			if self._show_files_button.GetLabelText() != text:
				self._show_files_button.SetLabelText( text )

			self._show_files_button.Show()

		else:
			self._show_files_button.Hide()

		popup_traceback = self._job_key.GetIfHasVariable( 'popup_traceback' )

		if popup_traceback is not None:
			self._copy_tb_button.Show()

		else:
			self._copy_tb_button.Hide()

		if popup_traceback is not None:
			text = popup_traceback

			self._tb_text.SetLabelText( self._ProcessText( text ) )

			self._show_tb_button.Show()

		else:
			self._show_tb_button.Hide()
			self._tb_text.Hide()

		if self._job_key.IsPausable():
			self._pause_button.Show()

		else:
			self._pause_button.Hide()

		if self._job_key.IsCancellable():
			self._cancel_button.Show()

		else:
			self._cancel_button.Hide()

class PopupMessageManager( wx.Frame ):
	def __init__( self, parent ):
		wx.Frame.__init__( self, parent, style = wx.FRAME_TOOL_WINDOW | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.BORDER_NONE )

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

		self._last_best_size_i_fit_on = ( 0, 0 )

		self._max_messages_to_display = 10

		vbox = wx.BoxSizer( wx.VERTICAL )

		self._message_vbox = wx.BoxSizer( wx.VERTICAL )

		self._dismiss_all = PopupDismissAll( self )
		self._dismiss_all.Hide()

		vbox.Add( self._message_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._dismiss_all, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		self._pending_job_keys = []

		parent.Bind( wx.EVT_SIZE, self.EventMove )
		parent.Bind( wx.EVT_MOVE, self.EventMove )

		HG.client_controller.sub( self, 'AddMessage', 'message' )

		self._old_excepthook = sys.excepthook
		self._old_show_exception = ShowException

		sys.excepthook = CatchExceptionClient
		ShowException = ShowExceptionClient
		ShowText = ShowTextClient

		job_key = JobKey()

		job_key.SetVariable( 'popup_text_1', 'initialising popup message manager\u2026' )

		self._update_job = HG.client_controller.CallRepeatingWXSafe( self, 0.25, 0.5, self.REPEATINGUpdate )

		HG.client_controller.CallLaterWXSafe( self, 0.5, self.AddMessage, job_key )

		HG.client_controller.CallLaterWXSafe( self, 1.0, job_key.Delete )

	def _CheckPending( self ):
		force_relayout = False

		self._pending_job_keys = [ job_key for job_key in self._pending_job_keys if not job_key.IsDeleted() ]

		while len( self._pending_job_keys ) > 0 and self._message_vbox.GetItemCount() < self._max_messages_to_display:
			job_key = self._pending_job_keys.pop( 0 )

			window = PopupMessage( self, job_key )

			window.UpdateMessage()

			self._message_vbox.Add( window, CC.FLAGS_EXPAND_PERPENDICULAR )

			force_relayout = True

		dismiss_shown_before = self._dismiss_all.IsShown()

		num_messages_pending = len( self._pending_job_keys )

		if num_messages_pending > 0:
			self._dismiss_all.SetNumMessages( num_messages_pending )

			self._dismiss_all.Show()

		else:
			self._dismiss_all.Hide()

		if self._dismiss_all.IsShown() != dismiss_shown_before:
			force_relayout = True

		if force_relayout:
			self.Layout()

		if self._NeedsSizeOrShow():
			self._SizeAndPositionAndShow()

	def _DisplayingError( self ):
		sizer_items = self._message_vbox.GetChildren()

		for sizer_item in sizer_items:
			message_window = sizer_item.GetWindow()

			job_key = message_window.GetJobKey()

			if job_key.HasVariable( 'popup_traceback' ):
				return True

		return False

	def _DoDebugHide( self ):
		parent = self.GetParent()

		parent_iconized = parent.IsIconized()

		# changing show status while parent iconised in Windows leads to grey window syndrome
		windows_and_iconised = HC.PLATFORM_WINDOWS and parent_iconized

		possibly_on_hidden_virtual_desktop = not MouseIsOnMyDisplay( parent )

		going_to_bug_out_at_hide_or_show = windows_and_iconised or possibly_on_hidden_virtual_desktop

		new_options = HG.client_controller.new_options

		if new_options.GetBoolean( 'hide_message_manager_on_gui_iconise' ):
			if parent.IsIconized():
				self.Hide()

				return

		current_focus_tlp = wx.GetTopLevelParent( wx.Window.FindFocus() )

		main_gui_is_active = current_focus_tlp in ( self, parent )

		if new_options.GetBoolean( 'hide_message_manager_on_gui_deactive' ):
			if not main_gui_is_active:
				if not going_to_bug_out_at_hide_or_show:
					self.Hide()

	def _NeedsSizeOrShow( self ):
		num_messages_displayed = self._message_vbox.GetItemCount()

		there_is_stuff_to_display = num_messages_displayed > 0

		is_shown = self.IsShown()

		if there_is_stuff_to_display:
			if not is_shown:
				return True

			best_size = self.GetBestSize()

			if best_size != self.GetSize():
				return True

		else:
			if is_shown:
				return True

		return False

	def _SizeAndPositionAndShow( self ):
		try:
			parent = self.GetParent()

			# changing show status while parent iconised in Windows leads to grey window syndrome
			windows_and_iconised = HC.PLATFORM_WINDOWS and parent.IsIconized()

			possibly_on_hidden_virtual_desktop = not MouseIsOnMyDisplay( parent )

			going_to_bug_out_at_hide_or_show = windows_and_iconised or possibly_on_hidden_virtual_desktop

			current_focus_tlp = wx.GetTopLevelParent( wx.Window.FindFocus() )

			main_gui_is_active = current_focus_tlp in ( self, parent )

			on_top_frame_is_active = False

			if not main_gui_is_active:
				c_f_tlp_is_child_frame_of_main_gui = isinstance( current_focus_tlp, wx.Frame ) and current_focus_tlp.GetParent() == parent

				if c_f_tlp_is_child_frame_of_main_gui and current_focus_tlp.GetWindowStyle() & wx.FRAME_FLOAT_ON_PARENT == wx.FRAME_FLOAT_ON_PARENT:
					on_top_frame_is_active = True

			num_messages_displayed = self._message_vbox.GetItemCount()

			there_is_stuff_to_display = num_messages_displayed > 0

			if there_is_stuff_to_display:
				# little catch here to try to stop the linux users who got infinitely expanding popups wew

				popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )

				wrap_width = ConvertTextToPixelWidth( self, popup_message_character_width )

				max_width = wrap_width * 1.2

				( best_width, best_height ) = self.GetBestSize()

				best_width = min( best_width, max_width )

				best_size = ( best_width, best_height )

				if best_size != self._last_best_size_i_fit_on:
					self._last_best_size_i_fit_on = best_size

					self.SetClientSize( best_size )

					self.Layout()

				( parent_width, parent_height ) = parent.GetClientSize()

				( my_width, my_height ) = self.GetClientSize()

				my_x = ( parent_width - my_width ) - 25
				my_y = ( parent_height - my_height ) - 5

				if parent.IsShown():
					my_position = ClientToScreen( parent, ( my_x, my_y ) )

					if my_position != self.GetPosition():
						self.SetPosition( my_position )

				# Unhiding tends to raise the main gui tlp, which is annoying if a media viewer window has focus
				show_is_not_annoying = main_gui_is_active or on_top_frame_is_active or self._DisplayingError()

				ok_to_show = show_is_not_annoying and not going_to_bug_out_at_hide_or_show

				if ok_to_show:
					was_hidden = not self.IsShown()

					self.Show()

					if was_hidden:
						self.Layout()

						self.Refresh()

			else:
				if not going_to_bug_out_at_hide_or_show:
					self.Hide()

		except Exception:
			text = 'The popup message manager experienced a fatal error and will now stop working! Please restart the client as soon as possible! If this keeps happening, please email the details and your client.log to the hydrus developer.'

			Print( text )

			Print( traceback.format_exc() )

			wx.MessageBox( text )

			self._update_job.Cancel()

			self.CleanBeforeDestroy()

			self.DestroyLater()

	def _GetAllMessageJobKeys( self ):
		job_keys = []

		sizer_items = self._message_vbox.GetChildren()

		for sizer_item in sizer_items:
			message_window = sizer_item.GetWindow()

			job_key = message_window.GetJobKey()

			job_keys.append( job_key )

		job_keys.extend( self._pending_job_keys )

		return job_keys

	def _OKToAlterUI( self ):
		main_gui = self.GetParent()

		not_on_hidden_or_virtual_display = MouseIsOnMyDisplay( main_gui )

		main_gui_up = not main_gui.IsIconized()

		return not_on_hidden_or_virtual_display and main_gui_up

	def _TryToMergeMessage( self, job_key ):
		if not job_key.HasVariable( 'popup_files_mergable' ):
			return False

		result = job_key.GetIfHasVariable( 'popup_files' )

		if result is not None:
			( hashes, name ) = result

			existing_job_keys = self._GetAllMessageJobKeys()

			for existing_job_key in existing_job_keys:
				if existing_job_key.HasVariable( 'popup_files_mergable' ):
					result = existing_job_key.GetIfHasVariable( 'popup_files' )

					if result is not None:
						( existing_hashes, existing_name ) = result

						if existing_name == name:
							if isinstance( existing_hashes, list ):
								existing_hashes.extend( hashes )

							elif isinstance( existing_hashes, set ):
								existing_hashes.update( hashes )

							return True

		return False

	def _Update( self ):
		if HG.view_shutdown:
			self._update_job.Cancel()

			self.CleanBeforeDestroy()

			self.DestroyLater()

			return

		sizer_items = self._message_vbox.GetChildren()

		for sizer_item in sizer_items:
			message_window = sizer_item.GetWindow()

			if message_window.IsDeleted():
				message_window.TryToDismiss()

				self.Layout()

				break

			else:
				message_window.UpdateMessage()

	def AddMessage( self, job_key ):
		try:
			was_merged = self._TryToMergeMessage( job_key )

			if was_merged:
				return

			self._pending_job_keys.append( job_key )

			if self._OKToAlterUI():
				self._CheckPending()

		except Exception:
			Print( traceback.format_exc() )

	def CleanBeforeDestroy( self ):
		for job_key in self._pending_job_keys:
			if job_key.IsCancellable():
				job_key.Cancel()

		sizer_items = self._message_vbox.GetChildren()

		for sizer_item in sizer_items:
			message_window = sizer_item.GetWindow()

			job_key = message_window.GetJobKey()

			if job_key.IsCancellable():
				job_key.Cancel()

		sys.excepthook = self._old_excepthook

		ShowException = self._old_show_exception

	def Dismiss( self, window ):
		self._message_vbox.Detach( window )

		window.DestroyLater()

		if self._OKToAlterUI():
			self.Layout()

			self._CheckPending()

	def DismissAll( self ):
		self._pending_job_keys = [ job_key for job_key in self._pending_job_keys if job_key.IsPausable() or job_key.IsCancellable() ]

		sizer_items = self._message_vbox.GetChildren()

		for sizer_item in sizer_items:
			message_window = sizer_item.GetWindow()

			message_window.TryToDismiss()

		self.Layout()

		self._CheckPending()

	def EventMove( self, event ):
		if self._OKToAlterUI():
			self._SizeAndPositionAndShow()

		event.Skip()

	def MakeSureEverythingFits( self ):
		if self._OKToAlterUI():
			self.Layout()

			self._SizeAndPositionAndShow()

	def REPEATINGUpdate( self ):
		try:
			self._DoDebugHide()

			if self._OKToAlterUI():
				self._Update()

				self._CheckPending()

		except Exception:
			self._update_job.Cancel()

			raise

class PopupMessageDialogPanel( ReviewPanel ):
	def __init__( self, parent, job_key ):
		ReviewPanel.__init__( self, parent )

		self._job_key = job_key

		self._message_window = PopupMessage( self, self._job_key )

		vbox = wx.BoxSizer( wx.VERTICAL )

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

		self.SetSizer( vbox )

		self._windows_hidden = []

		self._HideOtherWindows()

		self._message_pubbed = False

		self._update_job = HG.client_controller.CallRepeatingWXSafe( self, 0.25, 0.5, self.REPEATINGUpdate )

	def _HideOtherWindows( self ):
		for tlw in wx.GetTopLevelWindows():
			if tlw == self:
				continue

			if not isinstance( tlw, ( Frame, NewDialog ) ):
				continue

			if IsWXAncestor( self, tlw, through_tlws = True ):
				continue

			if isinstance( tlw, FrameGUI ):
				continue

			if not tlw.IsShown() or tlw.IsIconized():
				continue

			tlw.Hide()

			self._windows_hidden.append( tlw )

	def _ReleaseMessage( self ):
		if not self._message_pubbed:
			HG.client_controller.pub( 'message', self._job_key )

			self._message_pubbed = True

			self._RestoreOtherWindows()

	def _RestoreOtherWindows( self ):
		for tlw in self._windows_hidden:
			tlw.Show()

		self._windows_hidden = []

	def _Update( self ):
		self._message_window.UpdateMessage()

		best_size = self.GetBestSize()

		if best_size != self.GetSize():
			PostSizeChangedEvent( self )

	def Dismiss( self, window ):
		return

	def TryToClose( self ):
		if self._job_key.IsDone():
			self._ReleaseMessage()

		else:
			if self._job_key.IsCancellable():
				with DialogYesNo( self, 'Cancel/stop job?' ) as dlg:
					if dlg.ShowModal() == wx.ID_YES:
						self._job_key.Cancel()

						self._ReleaseMessage()

					else:
						raise VetoException()

			else:
				raise VetoException( 'Unfortunately, this job cannot be cancelled. If it really is taking too long, please kill the client through task manager.' )

	def REPEATINGUpdate( self ):
		try:
			if self._job_key.IsDone():
				parent = self.GetParent()

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

					parent.DoOK()

			else:
				self._Update()

		except Exception:
			self._update_job.Cancel()

			raise


code:
import re, wx, wx.adv

from lib.GUI.Common import BetterCheckListBox, BetterChoice, BetterRadioBox, \
	BetterStaticText, RatingLikeDialog, RatingNumericalDialog
from lib.GUI.Controls import BytesControl
from lib.GUI.OptionsPanels import OptionsPanelMimes
from lib.GUI.ScrolledPanels import EditPanel
from lib.GUI.Shortcuts import ConvertKeyEventToSimpleTuple
from lib.GUI.Time import TimeDeltaCtrl

from lib.Hydrus.Data import ConvertIntToPixels, ConvertPixelsToInt
from lib.Hydrus.Ratings import DISLIKE, LIKE, NULL, SET
from lib.Hydrus.Search import Predicate
from lib.Hydrus.Text import HexFilter

### CONSTANTS ###

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

### CLASSES ###

class InputFileSystemPredicate( EditPanel ):
	def __init__( self, parent, predicate_type ):
		EditPanel.__init__( self, parent )

		self._predicates = []

		pred_classes = []

		if predicate_type == HC.PREDICATE_TYPE_SYSTEM_AGE:
			pred_classes.append( PanelPredicateSystemAgeDelta )
			pred_classes.append( PanelPredicateSystemAgeDate )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_DIMENSIONS:
			pred_classes.append( PanelPredicateSystemHeight )
			pred_classes.append( PanelPredicateSystemWidth )
			pred_classes.append( PanelPredicateSystemRatio )
			pred_classes.append( PanelPredicateSystemNumPixels )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_DURATION:
			pred_classes.append( PanelPredicateSystemDuration )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE:
			pred_classes.append( PanelPredicateSystemFileService )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
			pred_classes.append( PanelPredicateSystemKnownURLsExactURL )
			pred_classes.append( PanelPredicateSystemKnownURLsDomain )
			pred_classes.append( PanelPredicateSystemKnownURLsRegex )
			pred_classes.append( PanelPredicateSystemKnownURLsURLMatch )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_HASH:
			pred_classes.append( PanelPredicateSystemHash )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_LIMIT:
			pred_classes.append( PanelPredicateSystemLimit )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_MIME:
			pred_classes.append( PanelPredicateSystemMime )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS:
			pred_classes.append( PanelPredicateSystemNumTags )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_NUM_WORDS:
			pred_classes.append( PanelPredicateSystemNumWords )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_RATING:
			services_manager = HG.client_controller.services_manager

			ratings_services = services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )

			if len( ratings_services ) > 0:
				pred_classes.append( PanelPredicateSystemRating )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
			pred_classes.append( PanelPredicateSystemSimilarTo )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIZE:
			pred_classes.append( PanelPredicateSystemSize )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
			pred_classes.append( PanelPredicateSystemTagAsNumber )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_DUPLICATE_RELATIONSHIPS:
			pred_classes.append( PanelPredicateSystemDuplicateRelationships )

		elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
			pred_classes.append( PanelPredicateSystemFileViewingStatsViews )
			pred_classes.append( PanelPredicateSystemFileViewingStatsViewtime )

		vbox = wx.BoxSizer( wx.VERTICAL )

		for pred_class in pred_classes:
			panel = self._Panel( self, pred_class )

			vbox.Add( panel, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

	def GetValue( self ):
		return self._predicates

	def SubPanelOK( self, predicates ):
		self._predicates = predicates

		self.GetParent().DoOK()

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

			self._predicate_panel = predicate_class( self )

			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 ) )

			hbox = wx.BoxSizer( wx.HORIZONTAL )

			hbox.Add( self._predicate_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
			hbox.Add( self._ok, CC.FLAGS_VCENTER )

			self.SetSizer( hbox )

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

		def _DoOK( self ):
			predicates = self._predicate_panel.GetPredicates()

			self.GetParent().SubPanelOK( predicates )

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

			if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
				self._DoOK()

			else:
				event.Skip()

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

class PanelPredicateSystem( wx.Panel ):
	PREDICATE_TYPE = None

	def GetInfo( self ):
		raise NotImplementedError()

	def GetPredicates( self ):
		info = self.GetInfo()

		predicates = ( Predicate( self.PREDICATE_TYPE, info ), )

		return predicates

class PanelPredicateSystemAgeDate( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_AGE

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._date = wx.adv.CalendarCtrl( self )

		wx_dt = wx.DateTime.Today()

		wx_dt.Subtract( wx.TimeSpan( 24 * 7 ) )

		self._date.SetDate( wx_dt )

		self._sign.SetStringSelection( '>' )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:time imported' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._date, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		wx_dt = self._date.GetDate()

		year = wx_dt.year
		month = wx_dt.month + 1 # month zero indexed, wew
		day = wx_dt.day

		info = ( self._sign.GetStringSelection(), 'date', ( year, month, day ) )

		return info

class PanelPredicateSystemAgeDelta( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_AGE

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '>' ] )

		self._years = wx.SpinCtrl( self, max = 30, size = ( 60, -1 ) )
		self._months = wx.SpinCtrl( self, max = 60, size = ( 60, -1 ) )
		self._days = wx.SpinCtrl( self, max = 90, size = ( 60, -1 ) )
		self._hours = wx.SpinCtrl( self, max = 24, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		try:
			( sign, age_type, ( years, months, days, hours ) ) = system_predicates[ 'age' ]

		except Exception:
			# wew lad. replace this all with proper system pred saving on new_options in future
			sign = '<'

			years = 0
			months = 0
			days = 7
			hours = 0

		self._sign.SetStringSelection( sign )

		self._years.SetValue( years )
		self._months.SetValue( months )
		self._days.SetValue( days )
		self._hours.SetValue( hours )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:time imported' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._years, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'years' ), CC.FLAGS_VCENTER )
		hbox.Add( self._months, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'months' ), CC.FLAGS_VCENTER )
		hbox.Add( self._days, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'days' ), CC.FLAGS_VCENTER )
		hbox.Add( self._hours, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'hours' ), CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._days.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), 'delta', ( self._years.GetValue(), self._months.GetValue(), self._days.GetValue(), self._hours.GetValue() ) )

		return info

class PanelPredicateSystemDuplicateRelationships( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_DUPLICATE_RELATIONSHIPS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		choices = [ '<', '\u2248', '=', '>' ]

		self._sign = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )

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

		choices = [ ( HC.duplicate_type_string_lookup[ status ], status ) for status in ( HC.DUPLICATE_BETTER_OR_WORSE, HC.DUPLICATE_BETTER, HC.DUPLICATE_WORSE, HC.DUPLICATE_SAME_QUALITY, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_NOT_DUPLICATE, HC.DUPLICATE_UNKNOWN ) ]

		self._dupe_type = BetterRadioBox( self, choices = choices, style = wx.RA_SPECIFY_ROWS )

		#

		self._sign.SetStringSelection( '>' )
		self._num.SetValue( 0 )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:num duplicate relationships' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._num, CC.FLAGS_VCENTER )
		hbox.Add( self._dupe_type, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._num.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._num.GetValue(), self._dupe_type.GetChoice() )

		return info

class PanelPredicateSystemDuration( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_DURATION

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		choices = [ '<', '\u2248', '=', '>' ]

		self._sign = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )

		self._duration_s = wx.SpinCtrl( self, max = 3599, size = ( 60, -1 ) )
		self._duration_ms = wx.SpinCtrl( self, max = 999, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, ms ) = system_predicates[ 'duration' ]

		s = ms // 1000

		ms = ms % 1000

		self._sign.SetStringSelection( sign )

		self._duration_s.SetValue( s )
		self._duration_ms.SetValue( ms )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:duration' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._duration_s, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 's' ), CC.FLAGS_VCENTER )
		hbox.Add( self._duration_ms, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'ms' ), CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._duration_s.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._duration_s.GetValue() * 1000 + self._duration_ms.GetValue() )

		return info

class PanelPredicateSystemFileService( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_FILE_SERVICE

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = BetterRadioBox( self, choices = [ ( 'is', True ), ( 'is not', False ) ], style = wx.RA_SPECIFY_ROWS )

		self._current_pending = BetterRadioBox( self, choices = [ ( 'currently in', HC.CONTENT_STATUS_CURRENT ), ( 'pending to', HC.CONTENT_STATUS_PENDING ) ], style = wx.RA_SPECIFY_ROWS )

		services = HG.client_controller.services_manager.GetServices( HC.FILE_SERVICES )

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

		self._file_service_key = BetterRadioBox( self, choices = choices, style = wx.RA_SPECIFY_ROWS )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:file service:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._current_pending, CC.FLAGS_VCENTER )
		hbox.Add( self._file_service_key, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._sign.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetChoice(), self._current_pending.GetChoice(), self._file_service_key.GetChoice() )

		return info

class PanelPredicateSystemFileViewingStatsViews( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._viewing_locations = BetterCheckListBox( self )

		self._viewing_locations.Append( 'media views', 'media' )
		self._viewing_locations.Append( 'preview views', 'preview' )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._value = wx.SpinCtrl( self, min = 0, max = 1000000 )

		#

		self._viewing_locations.Check( 0 )

		self._sign.Select( 3 )

		self._value.SetValue( 10 )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._viewing_locations, CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._value, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		viewing_locations = self._viewing_locations.GetChecked()

		if len( viewing_locations ) == 0:
			viewing_locations = [ 'media' ]

		sign = self._sign.GetStringSelection()

		value = self._value.GetValue()

		info = ( 'views', tuple( viewing_locations ), sign, value )

		return info

class PanelPredicateSystemFileViewingStatsViewtime( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._viewing_locations = BetterCheckListBox( self )

		self._viewing_locations.Append( 'media viewtime', 'media' )
		self._viewing_locations.Append( 'preview viewtime', 'preview' )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._time_delta = TimeDeltaCtrl( self, min = 0, days = True, hours = True, minutes = True, seconds = True )

		#

		self._viewing_locations.Check( 0 )

		self._sign.Select( 3 )

		self._time_delta.SetValue( 600 )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._viewing_locations, CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._time_delta, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		viewing_locations = self._viewing_locations.GetChecked()

		if len( viewing_locations ) == 0:
			viewing_locations = [ 'media' ]

		sign = self._sign.GetStringSelection()

		time_delta = self._time_delta.GetValue()

		info = ( 'viewtime', tuple( viewing_locations ), sign, time_delta )

		return info

class PanelPredicateSystemHash( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_HASH

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self.SetToolTip( 'As this can only ever return one result, it overrules the active file domain and any other active predicate.' )

		self._hash = wx.TextCtrl( self, size = ( 200, -1 ) )

		choices = [ 'sha256', 'md5', 'sha1', 'sha512' ]

		self._hash_type = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:hash=' ), CC.FLAGS_VCENTER )
		hbox.Add( self._hash, CC.FLAGS_VCENTER )
		hbox.Add( self._hash_type, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._hash.SetFocus )

	def GetInfo( self ):
		hex_hash = self._hash.GetValue().lower()

		hex_hash = HexFilter( hex_hash )

		if len( hex_hash ) == 0:
			hex_hash = '00'

		elif len( hex_hash ) % 2 == 1:
			hex_hash += '0' # since we are later decoding to byte

		hash = bytes.fromhex( hex_hash )

		hash_type = self._hash_type.GetStringSelection()

		return ( hash, hash_type )

class PanelPredicateSystemHeight( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_HEIGHT

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._height = wx.SpinCtrl( self, max = 200000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, height ) = system_predicates[ 'height' ]

		self._sign.SetStringSelection( sign )

		self._height.SetValue( height )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:height' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._height, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._height.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._height.GetValue() )

		return info

class PanelPredicateSystemKnownURLsExactURL( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._operator = BetterChoice( self )

		self._operator.Append( 'has', True )
		self._operator.Append( 'does not have', False )

		self._exact_url = wx.TextCtrl( self, size = ( 250, -1 ) )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:known url' ), CC.FLAGS_VCENTER )
		hbox.Add( self._operator, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'exact url:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._exact_url, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		operator = self._operator.GetChoice()

		if operator:
			operator_description = 'has this url: '

		else:
			operator_description = 'does not have this url: '

		rule_type = 'regex'

		exact_url = self._exact_url.GetValue()

		rule = re.escape( exact_url )

		description = operator_description + exact_url

		return ( operator, rule_type, rule, description )

class PanelPredicateSystemKnownURLsDomain( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._operator = BetterChoice( self )

		self._operator.Append( 'has', True )
		self._operator.Append( 'does not have', False )

		self._domain = wx.TextCtrl( self, size = ( 250, -1 ) )

		self._domain.SetValue( 'example.com' )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:known url' ), CC.FLAGS_VCENTER )
		hbox.Add( self._operator, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'a url with domain:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._domain, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		operator = self._operator.GetChoice()

		if operator:
			operator_description = 'has a url with domain: '

		else:
			operator_description = 'does not have a url with domain: '

		rule_type = 'regex'

		domain = self._domain.GetValue()

		rule = r'^https?\:\/\/( www[^\.]*\. )?' + re.escape( domain ) + r'\/.*'

		description = operator_description + domain

		return ( operator, rule_type, rule, description )

class PanelPredicateSystemKnownURLsRegex( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._operator = BetterChoice( self )

		self._operator.Append( 'has', True )
		self._operator.Append( 'does not have', False )

		self._regex = wx.TextCtrl( self, size = ( 250, -1 ) )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:known url' ), CC.FLAGS_VCENTER )
		hbox.Add( self._operator, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'a url that matches this regex:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._regex, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		operator = self._operator.GetChoice()

		if operator:
			operator_description = 'has a url matching regex: '

		else:
			operator_description = 'does not have a url matching regex: '

		rule_type = 'regex'

		regex = self._regex.GetValue()

		rule = regex

		description = operator_description + regex

		return ( operator, rule_type, rule, description )

class PanelPredicateSystemKnownURLsURLMatch( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_KNOWN_URLS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._operator = BetterChoice( self )

		self._operator.Append( 'has', True )
		self._operator.Append( 'does not have', False )

		self._url_matches = BetterChoice( self )

		for url_match in HG.client_controller.network_engine.domain_manager.GetURLMatches():
			if url_match.ShouldAssociateWithFiles():
				self._url_matches.Append( url_match.GetName(), url_match )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:known url' ), CC.FLAGS_VCENTER )
		hbox.Add( self._operator, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, 'url matching this class:' ), CC.FLAGS_VCENTER )
		hbox.Add( self._url_matches, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		operator = self._operator.GetChoice()

		if operator:
			operator_description = 'has '

		else:
			operator_description = 'does not have '

		rule_type = 'url_match'

		url_match = self._url_matches.GetChoice()

		rule = url_match

		description = operator_description + url_match.GetName() + ' url'

		return ( operator, rule_type, rule, description )

class PanelPredicateSystemLimit( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_LIMIT

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._limit = wx.SpinCtrl( self, max = 1000000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		limit = system_predicates[ 'limit' ]

		self._limit.SetValue( limit )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:limit=' ), CC.FLAGS_VCENTER )
		hbox.Add( self._limit, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._limit.SetFocus )

	def GetInfo( self ):
		info = self._limit.GetValue()

		return info

class PanelPredicateSystemMime( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_MIME

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._mimes = OptionsPanelMimes( self, HC.SEARCHABLE_MIMES )

		system_predicates = HC.options[ 'file_system_predicates' ]

		mimes = system_predicates[ 'mime' ]

		if isinstance( mimes, int ):
			mimes = ( mimes, )

		self._mimes.SetValue( mimes )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:mime' ), CC.FLAGS_VCENTER )
		hbox.Add( self._mimes, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

	def GetInfo( self ):
		mimes = self._mimes.GetValue()

		return mimes

class PanelPredicateSystemNumPixels( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_NUM_PIXELS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._num_pixels = wx.SpinCtrl( self, max = 1048576, size = ( 60, -1 ) )

		self._unit = wx.RadioBox( self, choices = [ 'pixels', 'kilopixels', 'megapixels' ] )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, num_pixels, unit ) = system_predicates[ 'num_pixels' ]

		self._sign.SetStringSelection( sign )

		self._num_pixels.SetValue( num_pixels )

		self._unit.SetStringSelection( ConvertIntToPixels( unit ) )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:num_pixels' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._num_pixels, CC.FLAGS_VCENTER )
		hbox.Add( self._unit, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._num_pixels.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._num_pixels.GetValue(), ConvertPixelsToInt( self._unit.GetStringSelection() ) )

		return info

class PanelPredicateSystemNumTags( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._num_tags = wx.SpinCtrl( self, max = 2000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, num_tags ) = system_predicates[ 'num_tags' ]

		self._sign.SetStringSelection( sign )

		self._num_tags.SetValue( num_tags )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:num_tags' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._num_tags, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._num_tags.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._num_tags.GetValue() )

		return info

class PanelPredicateSystemNumWords( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_NUM_WORDS

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._num_words = wx.SpinCtrl( self, max = 1000000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, num_words ) = system_predicates[ 'num_words' ]

		self._sign.SetStringSelection( sign )

		self._num_words.SetValue( num_words )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:num_words' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._num_words, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._num_words.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._num_words.GetValue() )

		return info

class PanelPredicateSystemRating( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_RATING

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		#

		local_like_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, ), randomised = False )

		self._like_checkboxes_to_info = {}

		self._like_rating_ctrls = []

		gridbox = wx.FlexGridSizer( 5 )

		gridbox.AddGrowableCol( 0, 1 )

		for service in local_like_services:
			name = service.GetName()
			service_key = service.GetServiceKey()

			rated_checkbox = wx.CheckBox( self, label = 'rated' )
			not_rated_checkbox = wx.CheckBox( self, label = 'not rated' )
			rating_ctrl = RatingLikeDialog( self, service_key )

			self._like_checkboxes_to_info[ rated_checkbox ] = ( service_key, SET )
			self._like_checkboxes_to_info[ not_rated_checkbox ] = ( service_key, NULL )
			self._like_rating_ctrls.append( rating_ctrl )

			gridbox.Add( BetterStaticText( self, name ), CC.FLAGS_VCENTER )
			gridbox.Add( rated_checkbox, CC.FLAGS_VCENTER )
			gridbox.Add( not_rated_checkbox, CC.FLAGS_VCENTER )
			gridbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
			gridbox.Add( rating_ctrl, CC.FLAGS_VCENTER )

		#

		local_numerical_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_NUMERICAL, ), randomised = False )

		self._numerical_checkboxes_to_info = {}

		self._numerical_rating_ctrls_to_info = {}

		for service in local_numerical_services:
			name = service.GetName()
			service_key = service.GetServiceKey()

			rated_checkbox = wx.CheckBox( self, label = 'rated' )
			not_rated_checkbox = wx.CheckBox( self, label = 'not rated' )
			choice = wx.RadioBox( self, choices = [ '>', '<', '=', '\u2248' ] )
			rating_ctrl = RatingNumericalDialog( self, service_key )

			choice.SetSelection( 2 )

			self._numerical_checkboxes_to_info[ rated_checkbox ] = ( service_key, SET )
			self._numerical_checkboxes_to_info[ not_rated_checkbox ] = ( service_key, NULL )
			self._numerical_rating_ctrls_to_info[ rating_ctrl ] = choice

			gridbox.Add( BetterStaticText( self, name ), CC.FLAGS_VCENTER )
			gridbox.Add( rated_checkbox, CC.FLAGS_VCENTER )
			gridbox.Add( not_rated_checkbox, CC.FLAGS_VCENTER )
			gridbox.Add( choice, CC.FLAGS_VCENTER )
			gridbox.Add( rating_ctrl, CC.FLAGS_VCENTER )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )

		self.SetSizer( vbox )

	def GetInfo( self ):
		infos = []

		#

		for ( checkbox, ( service_key, rating_state ) ) in list( self._like_checkboxes_to_info.items() ):
			if checkbox.GetValue() is True:
				if rating_state == SET:
					value = 'rated'

				elif rating_state == NULL:
					value = 'not rated'

				infos.append( ( '=', value, service_key ) )

		for ctrl in self._like_rating_ctrls:
			rating_state = ctrl.GetRatingState()

			if rating_state in ( LIKE, DISLIKE ):
				if rating_state == LIKE:
					value = 1

				elif rating_state == DISLIKE:
					value = 0

				service_key = ctrl.GetServiceKey()

				infos.append( ( '=', value, service_key ) )

		#

		for ( checkbox, ( service_key, rating_state ) ) in list( self._numerical_checkboxes_to_info.items() ):
			if checkbox.GetValue() is True:
				if rating_state == SET:
					value = 'rated'

				elif rating_state == NULL:
					value = 'not rated'

				infos.append( ( '=', value, service_key ) )

		for ( ctrl, choice ) in list( self._numerical_rating_ctrls_to_info.items() ):
			rating_state = ctrl.GetRatingState()

			if rating_state == SET:
				operator = choice.GetStringSelection()

				value = ctrl.GetRating()

				service_key = ctrl.GetServiceKey()

				infos.append( ( operator, value, service_key ) )

		#

		return infos

	def GetPredicates( self ):
		infos = self.GetInfo()

		predicates = [ Predicate( self.PREDICATE_TYPE, info ) for info in infos ]

		return predicates

class PanelPredicateSystemRatio( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_RATIO

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '=', 'wider than', 'taller than', '\u2248' ] )

		self._width = wx.SpinCtrl( self, max = 50000, size = ( 60, -1 ) )

		self._height = wx.SpinCtrl( self, max = 50000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, width, height ) = system_predicates[ 'ratio' ]

		self._sign.SetStringSelection( sign )

		self._width.SetValue( width )

		self._height.SetValue( height )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:ratio' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._width, CC.FLAGS_VCENTER )
		hbox.Add( BetterStaticText( self, ':' ), CC.FLAGS_VCENTER )
		hbox.Add( self._height, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._sign.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._width.GetValue(), self._height.GetValue() )

		return info

class PanelPredicateSystemSimilarTo( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._hash = wx.TextCtrl( self )

		self._max_hamming = wx.SpinCtrl( self, max = 256, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		self._hash.SetValue( 'enter hash' )

		hamming_distance = system_predicates[ 'hamming_distance' ]

		self._max_hamming.SetValue( hamming_distance )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:similar_to' ), CC.FLAGS_VCENTER )
		hbox.Add( self._hash, CC.FLAGS_VCENTER )
		hbox.Add( wx.StaticText( self, label='\u2248' ), CC.FLAGS_VCENTER )
		hbox.Add( self._max_hamming, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._hash.SetFocus )

	def GetInfo( self ):
		hex_hash = self._hash.GetValue()

		hex_hash = HexFilter( hex_hash )

		if len( hex_hash ) == 0:
			hex_hash = '00'

		elif len( hex_hash ) % 2 == 1:
			hex_hash += '0' # since we are later decoding to byte

		info = ( bytes.fromhex( hex_hash ), self._max_hamming.GetValue() )

		return info

class PanelPredicateSystemSize( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_SIZE

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._bytes = BytesControl( self )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, size, unit ) = system_predicates[ 'size' ]

		self._sign.SetStringSelection( sign )

		self._bytes.SetSeparatedValue( size, unit )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:size' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._bytes, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._bytes.SetFocus )

	def GetInfo( self ):
		( size, unit ) = self._bytes.GetSeparatedValue()

		info = ( self._sign.GetStringSelection(), size, unit )

		return info

class PanelPredicateSystemTagAsNumber( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._namespace = wx.TextCtrl( self )

		choices = [ '<', '\u2248', '>' ]

		self._sign = wx.RadioBox( self, choices = choices, style = wx.RA_SPECIFY_COLS )

		self._num = wx.SpinCtrl( self, min = -99999999, max = 99999999 )

		#

		self._namespace.SetValue( 'page' )
		self._sign.SetStringSelection( '>' )
		self._num.SetValue( 0 )

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:tag as number' ), CC.FLAGS_VCENTER )
		hbox.Add( self._namespace, CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._num, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._num.SetFocus )

	def GetInfo( self ):
		info = ( self._namespace.GetValue(), self._sign.GetStringSelection(), self._num.GetValue() )

		return info

class PanelPredicateSystemWidth( PanelPredicateSystem ):
	PREDICATE_TYPE = HC.PREDICATE_TYPE_SYSTEM_WIDTH

	def __init__( self, parent ):
		PanelPredicateSystem.__init__( self, parent )

		self._sign = wx.RadioBox( self, choices = [ '<', '\u2248', '=', '>' ] )

		self._width = wx.SpinCtrl( self, max = 200000, size = ( 60, -1 ) )

		system_predicates = HC.options[ 'file_system_predicates' ]

		( sign, width ) = system_predicates[ 'width' ]

		self._sign.SetStringSelection( sign )

		self._width.SetValue( width )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( BetterStaticText( self, 'system:width' ), CC.FLAGS_VCENTER )
		hbox.Add( self._sign, CC.FLAGS_VCENTER )
		hbox.Add( self._width, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		wx.CallAfter( self._width.SetFocus )

	def GetInfo( self ):
		info = ( self._sign.GetStringSelection(), self._width.GetValue() )

		return info

license: wtfpl


code:
import os, threading, time, wx

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

from lib.Handling.Image import Dequantize, GetGIFFrameDurations
from lib.Handling.Image.Local import GenerateNumpyImage, ResizeNumpyImageForMediaViewer
from lib.Handling.Video import VideoRendererFFMPEG
from lib.Handling.Vide.Local import GIFRenderer

from lib.Hydrus.Data import ShowException, ShowText

LZ4_OK = False

try:
	import lz4
	import lz4.block

	LZ4_OK = True

except Exception: # ImportError wasn't enough here as Linux went up the shoot with a __version__ doesn't exist bs
	pass

### FUNCTIONS ###

def FrameIndexOutOfRange( index, range_start, range_end ):
	before_start = index < range_start
	after_end = range_end < index

	if range_start < range_end:
		if before_start or after_end:
			return True

	else:
		if after_end and before_start:
			return True

	return False

def GenerateHydrusBitmap( path, mime, compressed = True ):
	numpy_image = GenerateNumpyImage( path, mime )

	return GenerateHydrusBitmapFromNumPyImage( numpy_image, compressed = compressed )

def GenerateHydrusBitmapFromNumPyImage( numpy_image, compressed = True ):
	( y, x, depth ) = numpy_image.shape

	return HydrusBitmap( numpy_image.data, ( x, y ), depth, compressed = compressed )

def GenerateHydrusBitmapFromPILImage( pil_image, compressed = True ):
	pil_image = Dequantize( pil_image )

	if pil_image.mode == 'RGBA':
		depth = 4

	elif pil_image.mode == 'RGB':
		depth = 3

	return HydrusBitmap( pil_image.tobytes(), pil_image.size, depth, compressed = compressed )

class ImageRenderer( object ):
	def __init__( self, media ):
		self._media = media
		self._numpy_image = None

		self._hash = self._media.GetHash()
		self._mime = self._media.GetMime()

		client_files_manager = HG.client_controller.client_files_manager

		self._path = client_files_manager.GetFilePath( self._hash, self._mime )

		HG.client_controller.CallToThread( self._Initialise )

	def _Initialise( self ):
		self._numpy_image = GenerateNumpyImage( self._path, self._mime )

	def GetEstimatedMemoryFootprint( self ):
		if self._numpy_image is None:
			( width, height ) = self.GetResolution()

			return width * height * 3

		else:
			( height, width, depth ) = self._numpy_image.shape

			return height * width * depth

	def GetHash( self ): return self._media.GetHash()

	def GetNumFrames( self ): return self._media.GetNumFrames()

	def GetResolution( self ): return self._media.GetResolution()

	def GetWXBitmap( self, target_resolution = None ):
		# add region param to this to allow clipping before resize

		if target_resolution is None:
			wx_numpy_image = self._numpy_image

		else:
			wx_numpy_image = ResizeNumpyImageForMediaViewer( self._media.GetMime(), self._numpy_image, target_resolution )

		( wx_height, wx_width, wx_depth ) = wx_numpy_image.shape

		wx_data = wx_numpy_image.data

		if wx_depth == 3:
			return HG.client_controller.bitmap_manager.GetBitmapFromBuffer( wx_width, wx_height, 24, wx_data )

		else:
			return HG.client_controller.bitmap_manager.GetBitmapFromBuffer( wx_width, wx_height, 32, wx_data )

	def IsReady( self ):
		return self._numpy_image is not None

class RasterContainer( object ):
	def __init__( self, media, target_resolution = None ):
		if target_resolution is None:
			target_resolution = media.GetResolution()

		( width, height ) = target_resolution

		if width == 0 or height == 0:
			target_resolution = ( 100, 100 )

		self._media = media

		( media_width, media_height ) = self._media.GetResolution()
		( target_width, target_height ) = target_resolution

		if target_width > media_width or target_height > media_height:
			target_resolution = self._media.GetResolution()

		self._target_resolution = target_resolution

		( target_width, target_height ) = target_resolution

		hash = self._media.GetHash()
		mime = self._media.GetMime()

		client_files_manager = HG.client_controller.client_files_manager

		self._path = client_files_manager.GetFilePath( hash, mime )

		width_zoom = target_width / media_width
		height_zoom = target_height / media_height

		self._zoom = min( ( width_zoom, height_zoom ) )

		if self._zoom > 1.0:
			self._zoom = 1.0

class RasterContainerVideo( RasterContainer ):
	def __init__( self, media, target_resolution = None, init_position = 0 ):
		RasterContainer.__init__( self, media, target_resolution )

		self._init_position = init_position

		self._initialised = False

		self._renderer = None

		self._frames = {}

		self._buffer_start_index = -1
		self._buffer_end_index = -1

		self._stop = False

		self._render_event = threading.Event()

		( x, y ) = self._target_resolution

		new_options = HG.client_controller.new_options

		video_buffer_size_mb = new_options.GetInteger( 'video_buffer_size_mb' )

		duration = self._media.GetDuration()
		num_frames_in_video = self._media.GetNumFrames()

		if duration is None or duration == 0:
			message = 'The file with hash ' + media.GetHash().hex() + ', had an invalid duration.'
			message += os.linesep * 2
			message += 'You may wish to try regenerating its metadata through the advanced mode right-click menu.'

			ShowText( message )

			duration = 1.0

		if num_frames_in_video is None or num_frames_in_video == 0:
			message = 'The file with hash ' + media.GetHash().hex() + ', had an invalid number of frames.'
			message += os.linesep * 2
			message += 'You may wish to try regenerating its metadata through the advanced mode right-click menu.'

			ShowText( message )

			num_frames_in_video = 1

		self._average_frame_duration = duration / num_frames_in_video

		frame_buffer_length = ( video_buffer_size_mb * 1024 * 1024 ) // ( x * y * 3 )

		# if we can't buffer the whole vid, then don't have a clunky massive buffer

		max_streaming_buffer_size = max( 48, int( num_frames_in_video / ( duration / 3.0 ) ) ) # 48 or 3 seconds

		if max_streaming_buffer_size < frame_buffer_length and frame_buffer_length < num_frames_in_video:
			frame_buffer_length = max_streaming_buffer_size

		self._num_frames_backwards = frame_buffer_length * 2 // 3
		self._num_frames_forwards = frame_buffer_length // 3

		self._lock = threading.Lock()

		self._last_index_rendered = -1
		self._next_render_index = -1
		self._rendered_first_frame = False
		self._ideal_next_frame = 0

		HG.client_controller.CallToThread( self.THREADRender )

	def _HasFrame( self, index ):
		return index in self._frames

	def _IndexInRange( self, index, range_start, range_end ):
		return not FrameIndexOutOfRange( index, range_start, range_end )

	def _MaintainBuffer( self ):
		deletees = [ index for index in list( self._frames.keys() ) if FrameIndexOutOfRange( index, self._buffer_start_index, self._buffer_end_index ) ]

		for i in deletees:
			del self._frames[ i ]

	def THREADRender( self ):
		# hash = self._media.GetHash()
		mime = self._media.GetMime()
		duration = self._media.GetDuration()
		num_frames_in_video = self._media.GetNumFrames()

		# client_files_manager = HG.client_controller.client_files_manager

		time.sleep( 0.00001 )

		if self._media.GetMime() == HC.IMAGE_GIF:
			self._durations = GetGIFFrameDurations( self._path )

			self._renderer = GIFRenderer( self._path, num_frames_in_video, self._target_resolution )

		else:
			self._renderer = VideoRendererFFMPEG( self._path, mime, duration, num_frames_in_video, self._target_resolution )

		# give ui a chance to draw a blank frame rather than hard-charge right into CPUland
		time.sleep( 0.00001 )

		self.GetReadyForFrame( self._init_position )

		with self._lock:
			self._initialised = True

		while True:
			if self._stop or HG.view_shutdown:
				self._renderer.Stop()

				self._renderer = None

				with self._lock:
					self._frames = {}

				return

			#

			with self._lock:
				# lets see if we should move the renderer to a new position

				next_render_is_out_of_buffer = FrameIndexOutOfRange( self._next_render_index, self._buffer_start_index, self._buffer_end_index )
				buffer_not_fully_rendered = self._last_index_rendered != self._buffer_end_index

				currently_rendering_out_of_buffer = next_render_is_out_of_buffer and buffer_not_fully_rendered

				will_render_ideal_frame_soon = self._IndexInRange( self._next_render_index, self._buffer_start_index, self._ideal_next_frame )

				need_ideal_next_frame = not self._HasFrame( self._ideal_next_frame )

				will_not_get_to_ideal_frame = need_ideal_next_frame and not will_render_ideal_frame_soon

				if currently_rendering_out_of_buffer or will_not_get_to_ideal_frame:
					# we cannot get to the ideal next frame, so we need to rewind/reposition

					self._renderer.set_position( self._buffer_start_index )

					self._last_index_rendered = -1

					self._next_render_index = self._buffer_start_index

				#

				need_to_render = self._last_index_rendered != self._buffer_end_index

			if need_to_render:
				with self._lock:
					self._rendered_first_frame = True

					frame_index = self._next_render_index # keep this before the get call, as it increments in a clock arithmetic way afterwards

					try:
						numpy_image = self._renderer.read_frame()

					except Exception as e:
						ShowException( e )

						return

					finally:
						self._last_index_rendered = frame_index

						self._next_render_index = ( self._next_render_index + 1 ) % num_frames_in_video

				with self._lock:
					if self._next_render_index == 0 and self._buffer_end_index != num_frames_in_video - 1:
						# we need to rewind renderer

						self._renderer.set_position( 0 )

						self._last_index_rendered = -1

					should_save_frame = not self._HasFrame( frame_index )

				if should_save_frame:
					frame = GenerateHydrusBitmapFromNumPyImage( numpy_image, compressed = False )

					with self._lock:
						self._frames[ frame_index ] = frame

						self._MaintainBuffer()

				with self._lock:
					work_still_to_do = self._last_index_rendered != self._buffer_end_index

				if work_still_to_do:
					time.sleep( 0.0001 )

				else:
					half_a_frame = ( self._average_frame_duration / 1000.0 ) * 0.5

					sleep_duration = min( 0.1, half_a_frame ) # for 10s-long 3-frame gifs, wew

					time.sleep( sleep_duration ) # just so we don't spam cpu

			else:
				self._render_event.wait( 1 )

				self._render_event.clear()

	def GetBufferIndices( self ):
		if self._last_index_rendered == -1:
			return None

		else:
			return ( self._buffer_start_index, self._last_index_rendered, self._buffer_end_index )

	def GetDuration( self, index ):
		if self._media.GetMime() == HC.IMAGE_GIF:
			return self._durations[ index ]

		else:
			return self._average_frame_duration

	def GetFrame( self, index ):
		with self._lock:
			frame = self._frames[ index ]

		num_frames_in_video = self.GetNumFrames()

		if index == num_frames_in_video - 1:
			next_index = 0

		else:
			next_index = index + 1

		self.GetReadyForFrame( next_index )

		return frame

	def GetHash( self ):
		return self._media.GetHash()

	def GetKey( self ):
		return ( self._media.GetHash(), self._target_resolution )

	def GetNumFrames( self ):
		return self._media.GetNumFrames()

	def GetReadyForFrame( self, next_index_to_expect ):
		num_frames_in_video = self.GetNumFrames()

		frame_request_is_impossible = FrameIndexOutOfRange( next_index_to_expect, 0, num_frames_in_video - 1 )

		if frame_request_is_impossible:
			return

		with self._lock:
			self._ideal_next_frame = next_index_to_expect

			video_is_bigger_than_buffer = num_frames_in_video > self._num_frames_backwards + 1 + self._num_frames_forwards

			if video_is_bigger_than_buffer:
				current_ideal_is_out_of_buffer = self._buffer_start_index == -1 or FrameIndexOutOfRange( self._ideal_next_frame, self._buffer_start_index, self._buffer_end_index )

				ideal_buffer_start_index = max( 0, self._ideal_next_frame - self._num_frames_backwards )

				ideal_buffer_end_index = ( self._ideal_next_frame + self._num_frames_forwards ) % num_frames_in_video

				if current_ideal_is_out_of_buffer:
					# the current buffer won't get to where we want, so remake it

					self._buffer_start_index = ideal_buffer_start_index
					self._buffer_end_index = ideal_buffer_end_index

				else:
					# we can get to our desired position, but should we move the start and beginning on a bit?

					# we do not ever want to shunt left (rewind)
					# we do not want to shunt right if we don't have the earliest frames yet--be patient

					# i.e. it is between the current start and the ideal
					next_ideal_start_would_shunt_right = self._IndexInRange( ideal_buffer_start_index, self._buffer_start_index, self._ideal_next_frame )
					have_next_ideal_start = self._HasFrame( ideal_buffer_start_index )

					if next_ideal_start_would_shunt_right and have_next_ideal_start:
						self._buffer_start_index = ideal_buffer_start_index

					next_ideal_end_would_shunt_right = self._IndexInRange( ideal_buffer_end_index, self._buffer_end_index, self._buffer_start_index )

					if next_ideal_end_would_shunt_right:
						self._buffer_end_index = ideal_buffer_end_index

			else:
				self._buffer_start_index = 0

				self._buffer_end_index = num_frames_in_video - 1

		self._render_event.set()

	def GetResolution( self ):
		return self._media.GetResolution()

	def GetSize( self ):
		return self._target_resolution

	def GetTimestampMS( self, frame_index ):
		if self._media.GetMime() == HC.IMAGE_GIF:
			return sum( self._durations[ : frame_index ] )

		else:
			return self._average_frame_duration * frame_index

	def GetTotalDuration( self ):
		if self._media.GetMime() == HC.IMAGE_GIF:
			return sum( self._durations )

		else:
			return self._average_frame_duration * self.GetNumFrames()

	def HasFrame( self, index ):
		with self._lock:
			return self._HasFrame( index )

	def CanHaveVariableFramerate( self ):
		with self._lock:
			return self._media.GetMime() == HC.IMAGE_GIF

	def IsInitialised( self ):
		with self._lock:
			return self._initialised

	def IsScaled( self ):
		return self._zoom != 1.0

	def Stop( self ):
		self._stop = True

class HydrusBitmap( object ):
	def __init__( self, data, size, depth, compressed = True ):
		if not LZ4_OK:
			compressed = False

		self._compressed = compressed

		if self._compressed:
			self._data = lz4.block.compress( data )

		else:
			self._data = data

		self._size = size
		self._depth = depth

	def _GetData( self ):
		if self._compressed:
			return lz4.block.decompress( self._data )

		else:
			return self._data

	def _GetWXBitmapFormat( self ):
		if self._depth == 3:
			return wx.BitmapBufferFormat_RGB

		elif self._depth == 4:
			return wx.BitmapBufferFormat_RGBA

	def CopyToWxBitmap( self, wx_bmp ):
		fmt = self._GetWXBitmapFormat()

		wx_bmp.CopyFromBuffer( self._GetData(), fmt )

	def GetDepth( self ):
		return self._depth

	def GetWxBitmap( self ):
		( width, height ) = self._size

		return HG.client_controller.bitmap_manager.GetBitmapFromBuffer( width, height, self._depth * 8, self._GetData() )

	def GetWxImage( self ):
		( width, height ) = self._size

		if self._depth == 3:
			return wx.ImageFromBuffer( width, height, self._GetData() )

		elif self._depth == 4:
			bitmap = HG.client_controller.bitmap_manager.GetBitmapFromBuffer( width, height, 32, self._GetData() )

			image = bitmap.ConvertToImage()

			HG.client_controller.bitmap_manager.ReleaseBitmap( bitmap )

			return image

	def GetEstimatedMemoryFootprint( self ):
		return len( self._data )

	def GetSize( self ):
		return self._size