code:
import wx, collections

from lib.Hydrus.Data import ShowText, Profile

### CONSTANTS ###

import lib.Hydrus.Globals as HG

menus_to_submenus = collections.defaultdict( set )
menus_to_menu_item_data = collections.defaultdict( set )

### FUNCTIONS ###

def AppendMenu( menu, submenu, label ):
	label = SanitiseLabel( label )

	menu.AppendSubMenu( submenu, label )

	menus_to_submenus[ menu ].add( submenu )

def AppendMenuBitmapItem( event_handler, menu, label, description, bitmap, callable, *args, **kwargs ):
	label = SanitiseLabel( label )

	menu_item = wx.MenuItem( menu, wx.ID_ANY, label )

	menu_item.SetHelp( description )

	menu_item.SetBitmap( bitmap )

	menu.Append( menu_item )

	BindMenuItem( event_handler, menu, menu_item, callable, *args, **kwargs )

	return menu_item

def AppendMenuCheckItem( event_handler, menu, label, description, initial_value, callable, *args, **kwargs ):
	label = SanitiseLabel( label )

	menu_item = menu.AppendCheckItem( wx.ID_ANY, label, description )

	menu_item.Check( initial_value )

	BindMenuItem( event_handler, menu, menu_item, callable, *args, **kwargs )

	return menu_item

def AppendMenuItem( event_handler, menu, label, description, callable, *args, **kwargs ):
	label = SanitiseLabel( label )

	menu_item = menu.Append( wx.ID_ANY, label, description )

	BindMenuItem( event_handler, menu, menu_item, callable, *args, **kwargs )

	return menu_item

def AppendMenuLabel( menu, label, description = None ):
	if description is None:
		description = ''

	menu_item = menu.Append( wx.ID_ANY, label, description )

	return menu_item

def AppendSeparator( menu ):
	num_items = menu.GetMenuItemCount()

	if num_items > 0:
		last_item = menu.FindItemByPosition( num_items - 1 )

		if not last_item.IsSeparator():
			menu.AppendSeparator()

def BindMenuItem( event_handler, menu, menu_item, callable, *args, **kwargs ):
	event_callable = GetEventCallable( callable, *args, **kwargs )

	event_handler.Bind( wx.EVT_MENU, event_callable, source = menu_item )

	menus_to_menu_item_data[ menu ].add( ( menu_item, event_handler ) )

def CreateMenu():
	menu = wx.Menu()

	return menu

def UnbindMenuItems( menu ):
	menu_item_data = menus_to_menu_item_data[ menu ]

	del menus_to_menu_item_data[ menu ]

	for ( menu_item, event_handler ) in menu_item_data:
		if not event_handler: # under some circumstances, this has been deleted before the menu was

			continue

		event_handler.Unbind( wx.EVT_MENU, source = menu_item )

	if menu in menus_to_submenus:
		submenus = menus_to_submenus[ menu ]

		for submenu in submenus:
			UnbindMenuItems( submenu )

			submenu.is_dead = True

		del menus_to_submenus[ menu ]

def DestroyMenu( window, menu ):
	UnbindMenuItems( menu )

	menu.is_dead = True

	# if the window we just popupmenu'd on is dead now (i.e. it died while the menu was open), destroying the menu will cause a crash and letting the event continue will cause a crash

	if not window:
		message = 'A window just died before its menu could be safely destroyed! If an exception were not raised here, the program would crash! If you know you did something tricky, please avoid this in future. If you think you did something normal, please let hydrus dev know.'

		raise Exception( message )

	menu.Destroy()

def GetEventCallable( callable, *args, **kwargs ):
	def event_callable( event ):
		if HG.menu_profile_mode:
			summary = 'Profiling menu: ' + repr( callable )

			ShowText( summary )

			Profile( summary, 'callable( *args, **kwargs )', globals(), locals() )

		else:
			callable( *args, **kwargs )

	return event_callable

def MenuIsDead( menu ):
	# doing 'if menu' doesn't work for the deadobject test, wew

	return hasattr( menu, 'is_dead' )

def SanitiseLabel( label ):
	if label == '':
		label = '-invalid label-'

	return label.replace( '&', '&&' )


code:
import wx, collections, wx.lib.masked.timectrl

### CONSTANTS ###

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

### CLASSES ###

class OptionsPanel( wx.Panel ):
	def GetOptions( self ): raise NotImplementedError()

	def SetOptions( self, options ): raise NotImplementedError()

	def GetValue( self ): raise NotImplementedError()

	def SetValue( self, info ): raise NotImplementedError()

class OptionsPanelMimes( OptionsPanel ):
	def __init__( self, parent, selectable_mimes ):
		OptionsPanel.__init__( self, parent )

		self._mimes_to_checkboxes = {}
		self._mime_groups_to_checkboxes = {}
		self._mime_groups_to_values = {}

		mime_groups = [ HC.APPLICATIONS, HC.AUDIO, HC.IMAGES, HC.VIDEO ]

		mime_groups_to_mimes = collections.defaultdict( list )

		for mime in selectable_mimes:
			for mime_group in mime_groups:
				if mime in mime_group:
					mime_groups_to_mimes[ mime_group ].append( mime )

					break

		gridbox = wx.FlexGridSizer( 2 )

		gridbox.AddGrowableCol( 1, 1 )

		for mime_group in mime_groups:
			mimes = mime_groups_to_mimes[ mime_group ]

			mg_checkbox = wx.CheckBox( self, label = HC.mime_string_lookup[ mime_group ] )
			mg_checkbox.Bind( wx.EVT_CHECKBOX, self.EventMimeGroupCheckbox )

			self._mime_groups_to_checkboxes[ mime_group ] = mg_checkbox
			self._mime_groups_to_values[ mime_group ] = mg_checkbox.GetValue()

			gridbox.Add( mg_checkbox, CC.FLAGS_VCENTER )

			vbox = wx.BoxSizer( wx.VERTICAL )

			for mime in mimes:
				m_checkbox = wx.CheckBox( self, label = HC.mime_string_lookup[ mime ] )
				m_checkbox.Bind( wx.EVT_CHECKBOX, self.EventMimeCheckbox )

				self._mimes_to_checkboxes[ mime ] = m_checkbox

				vbox.Add( m_checkbox, CC.FLAGS_EXPAND_PERPENDICULAR )

			gridbox.Add( vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )

		self.SetSizer( gridbox )

	def _UpdateMimeGroupCheckboxes( self ):
		for ( mime_group, mg_checkbox ) in list( self._mime_groups_to_checkboxes.items() ):
			respective_checkbox_values = [ m_checkbox.GetValue() for ( mime, m_checkbox ) in list( self._mimes_to_checkboxes.items() ) if mime in mime_group ]

			all_true = False not in respective_checkbox_values

			mg_checkbox.SetValue( all_true )
			self._mime_groups_to_values[ mime_group ] = all_true

	def EventMimeCheckbox( self, event ):
		self._UpdateMimeGroupCheckboxes()

	def EventMimeGroupCheckbox( self, event ):
		# this is a commandevent, which won't give up the checkbox object, so we have to do some jiggery pokery

		for ( mime_group, mg_checkbox ) in list( self._mime_groups_to_checkboxes.items() ):
			expected_value = self._mime_groups_to_values[ mime_group ]
			actual_value = mg_checkbox.GetValue()

			if actual_value != expected_value:
				for ( mime, m_checkbox ) in list( self._mimes_to_checkboxes.items() ):
					if mime in mime_group:
						m_checkbox.SetValue( actual_value )

				self._mime_groups_to_values[ mime_group ] = actual_value

	def GetValue( self ):
		mimes = tuple( [ mime for ( mime, checkbox ) in list( self._mimes_to_checkboxes.items() ) if checkbox.GetValue() is True ] )

		return mimes

	def SetValue( self, mimes ):
		for ( mime, checkbox ) in list( self._mimes_to_checkboxes.items() ):
			if mime in mimes:
				checkbox.SetValue( True )

			else:
				checkbox.SetValue( False )

		self._UpdateMimeGroupCheckboxes()

license: wtfpl


code:
import os, wx, collections, hashlib

from lib.GUI.Canvas import CanvasPanel
from lib.GUI.Common import ClientToScreen, IsWXAncestor, NotebookScreenToHitTest
from lib.GUI.Dialogs import Dialog, DialogTextEntry, DialogYesNo
from lib.GUI.Management import CreateManagementControllerDuplicateFilter, \
	CreateManagementControllerImportGallery, CreateManagementControllerImportMultipleWatcher, \
	CreateManagementControllerImportSimpleDownloader, \
	CreateManagementControllerImportURLs, \
	CreateManagementControllerPetitions, CreateManagementControllerQuery, \
	CreateManagementPanel, MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER, \
	MANAGEMENT_TYPE_IMPORT_URLS
from lib.GUI.Media import MediaPanelThumbnails
from lib.GUI.Menus import AppendMenu, AppendMenuItem, AppendSeparator
from lib.GUI.Shortcuts import ConvertKeyEventToSimpleTuple

from lib.Hydrus.Data import ConvertValueRangeToPrettyString, ShowException, \
	ShowText, SplitListIntoChunks, ToHumanInt
from lib.Hydrus.Exceptions import DataMissing, VetoException
from lib.Hydrus.Search import FileSearchContext
from lib.Hydrus.Serialisable import CreateFromSerialisableTuple, \
	HG.SerialisableType["GUI_SESSION"], SERIALISABLE_TYPES_TO_OBJECT_TYPES, \
	SerialisableBaseNamed

### CONSTANTS ###

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

RESERVED_SESSION_NAMES = { '', 'just a blank page', 'last session', 'exit session' }

### CLASSES ###

class DialogPageChooser( Dialog ):
	def __init__( self, parent, controller ):
		Dialog.__init__( self, parent, 'new page', position = 'center' )

		self._controller = controller

		self._result = None

		# spawn in this order, so focus precipitates from the graphical top

		self._button_7 = wx.Button( self, label = '', id = 7 )
		self._button_8 = wx.Button( self, label = '', id = 8 )
		self._button_9 = wx.Button( self, label = '', id = 9 )
		self._button_4 = wx.Button( self, label = '', id = 4 )
		self._button_5 = wx.Button( self, label = '', id = 5 )
		self._button_6 = wx.Button( self, label = '', id = 6 )
		self._button_1 = wx.Button( self, label = '', id = 1 )
		self._button_2 = wx.Button( self, label = '', id = 2 )
		self._button_3 = wx.Button( self, label = '', id = 3 )

		gridbox = wx.GridSizer( 3 )

		gridbox.Add( self._button_7, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_8, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_9, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_4, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_5, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_6, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_1, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_2, CC.FLAGS_EXPAND_BOTH_WAYS )
		gridbox.Add( self._button_3, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( gridbox )

		self.SetInitialSize( ( 420, 210 ) )

		self._services = HG.client_controller.services_manager.GetServices()

		repository_petition_permissions = [ ( content_type, HC.PERMISSION_ACTION_OVERRULE ) for content_type in HC.REPOSITORY_CONTENT_TYPES ]

		self._petition_service_keys = [ service.GetServiceKey() for service in self._services if service.GetServiceType() in HC.REPOSITORIES and True in ( service.HasPermission( content_type, action ) for ( content_type, action ) in repository_petition_permissions ) ]

		self._InitButtons( 'home' )

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

		#

		self.Show( True )

	def _AddEntry( self, button, entry ):
		id = button.GetId()

		self._command_dict[ id ] = entry

		( entry_type, obj ) = entry

		if entry_type == 'menu':
			button.SetLabelText( obj )

		elif entry_type == 'page_duplicate_filter':
			button.SetLabelText( 'duplicates processing' )

		elif entry_type == 'pages_notebook':
			button.SetLabelText( 'page of pages' )

		elif entry_type in ( 'page_query', 'page_petitions' ):
			name = HG.client_controller.services_manager.GetService( obj ).GetName()

			button.SetLabelText( name )

		elif entry_type == 'page_import_gallery':
			button.SetLabelText( 'gallery' )

		elif entry_type == 'page_import_simple_downloader':
			button.SetLabelText( 'simple downloader' )

		elif entry_type == 'page_import_watcher':
			button.SetLabelText( 'watcher' )

		elif entry_type == 'page_import_urls':
			button.SetLabelText( 'urls' )

		button.Show()

	def _HitButton( self, id ):
		if id in self._command_dict:
			( entry_type, obj ) = self._command_dict[ id ]

			if entry_type == 'menu':
				self._InitButtons( obj )

			else:
				if entry_type == 'page_query':
					file_service_key = obj

					page_name = 'files'

					search_enabled = True

					new_options = self._controller.new_options

					tag_service_key = new_options.GetKey( 'default_tag_service_search_page' )

					if not self._controller.services_manager.ServiceExists( tag_service_key ):
						tag_service_key = CC.COMBINED_TAG_SERVICE_KEY

					file_search_context = FileSearchContext( file_service_key = file_service_key, tag_service_key = tag_service_key )

					self._result = ( 'page', CreateManagementControllerQuery( page_name, file_service_key, file_search_context, search_enabled ) )

				elif entry_type == 'page_duplicate_filter':
					self._result = ( 'page', CreateManagementControllerDuplicateFilter() )

				elif entry_type == 'pages_notebook':
					self._result = ( 'pages', None )

				elif entry_type == 'page_import_gallery':
					self._result = ( 'page', CreateManagementControllerImportGallery() )

				elif entry_type == 'page_import_simple_downloader':
					self._result = ( 'page', CreateManagementControllerImportSimpleDownloader() )

				elif entry_type == 'page_import_watcher':
					self._result = ( 'page', CreateManagementControllerImportMultipleWatcher() )

				elif entry_type == 'page_import_urls':
					self._result = ( 'page', CreateManagementControllerImportURLs() )

				elif entry_type == 'page_petitions':
					petition_service_key = obj

					self._result = ( 'page', CreateManagementControllerPetitions( petition_service_key ) )

				self.EndModal( wx.ID_OK )

	def _InitButtons( self, menu_keyword ):
		self._command_dict = {}

		entries = []

		if menu_keyword == 'home':
			entries.append( ( 'menu', 'files' ) )
			entries.append( ( 'menu', 'download' ) )

			if len( self._petition_service_keys ) > 0:
				entries.append( ( 'menu', 'petitions' ) )

			entries.append( ( 'menu', 'special' ) )

		elif menu_keyword == 'files':
			entries.append( ( 'page_query', CC.LOCAL_FILE_SERVICE_KEY ) )
			entries.append( ( 'page_query', CC.TRASH_SERVICE_KEY ) )
			entries.append( ( 'page_query', CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )

			for service in self._services:
				if service.GetServiceType() == HC.FILE_REPOSITORY:
					entries.append( ( 'page_query', service.GetServiceKey() ) )

		elif menu_keyword == 'download':
			entries.append( ( 'page_import_urls', None ) )
			entries.append( ( 'page_import_watcher', None ) )
			entries.append( ( 'page_import_gallery', None ) )
			entries.append( ( 'page_import_simple_downloader', None ) )

		elif menu_keyword == 'petitions':
			entries = [ ( 'page_petitions', service_key ) for service_key in self._petition_service_keys ]

		elif menu_keyword == 'special':
			entries.append( ( 'pages_notebook', None ) )
			entries.append( ( 'page_duplicate_filter', None ) )

		if len( entries ) <= 4:
			self._button_1.Hide()
			self._button_3.Hide()
			self._button_5.Hide()
			self._button_7.Hide()
			self._button_9.Hide()

			potential_buttons = [ self._button_8, self._button_4, self._button_6, self._button_2 ]

		elif len( entries ) <= 9:
			potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ]

		else:
			# sort out a multi-page solution? maybe only if this becomes a big thing; the person can always select from the menus, yeah?

			potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ]
			entries = entries[:9]

		for entry in entries:
			self._AddEntry( potential_buttons.pop( 0 ), entry )

		unused_buttons = potential_buttons

		for button in unused_buttons:
			button.Hide()

	def EventButton( self, event ):
		id = event.GetId()

		if id == wx.ID_CANCEL:
			self.EndModal( wx.ID_CANCEL )

		else:
			self._HitButton( id )

	def EventCharHook( self, event ):
		id = None

		( modifier, key ) = ConvertKeyEventToSimpleTuple( event )

		if key == wx.WXK_UP:
			id = 8
		elif key == wx.WXK_LEFT:
			id = 4
		elif key == wx.WXK_RIGHT:
			id = 6
		elif key == wx.WXK_DOWN:
			id = 2
		elif key == wx.WXK_NUMPAD1:
			id = 1
		elif key == wx.WXK_NUMPAD2:
			id = 2
		elif key == wx.WXK_NUMPAD3:
			id = 3
		elif key == wx.WXK_NUMPAD4:
			id = 4
		elif key == wx.WXK_NUMPAD5:
			id = 5
		elif key == wx.WXK_NUMPAD6:
			id = 6
		elif key == wx.WXK_NUMPAD7:
			id = 7
		elif key == wx.WXK_NUMPAD8:
			id = 8
		elif key == wx.WXK_NUMPAD9:
			id = 9
		elif key == wx.WXK_ESCAPE:
			self.EndModal( wx.ID_CANCEL )

			return

		else:
			event.Skip()

		if id is not None:
			self._HitButton( id )

	def GetValue( self ):
		return self._result

class Page( wx.SplitterWindow ):
	def __init__( self, parent, controller, management_controller, initial_hashes ):
		wx.SplitterWindow.__init__( self, parent )

		self._controller = controller

		self._page_key = self._controller.AcquirePageKey()

		self._management_controller = management_controller

		self._initial_hashes = initial_hashes

		self._management_controller.SetKey( 'page', self._page_key )

		self._initialised = False

		self._pretty_status = ''

		self.SetMinimumPaneSize( 120 )
		self.SetSashGravity( 0.0 )

		self.Bind( wx.EVT_SPLITTER_DCLICK, self.EventUnsplit )

		self._search_preview_split = wx.SplitterWindow( self, style = wx.SP_NOBORDER )

		self._search_preview_split.SetMinimumPaneSize( 180 )
		self._search_preview_split.SetSashGravity( 1.0 )

		self._search_preview_split.Bind( wx.EVT_SPLITTER_DCLICK, self.EventPreviewUnsplit )

		self._management_panel = CreateManagementPanel( self._search_preview_split, self, self._controller, self._management_controller )

		file_service_key = self._management_controller.GetKey( 'file_service' )

		self._preview_panel = CanvasPanel( self._search_preview_split, self._page_key )

		self._media_panel = MediaPanelThumbnails( self, self._page_key, file_service_key, [] )

		self._search_preview_split.SplitHorizontally( self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )

		self.SplitVertically( self._search_preview_split, self._media_panel, HC.options[ 'hpos' ] )

		if HC.options[ 'hide_preview' ]:
			wx.CallAfter( self._search_preview_split.Unsplit, self._preview_panel )

		self._controller.sub( self, 'SetPrettyStatus', 'new_page_status' )
		self._controller.sub( self, 'SetSplitterPositions', 'set_splitter_positions' )

	def _SetPrettyStatus( self, status ):
		self._pretty_status = status

		self._controller.gui.SetStatusBarDirty()

	def _SwapMediaPanel( self, new_panel ):
		# if a new media page comes in while its menu is open, we can enter program instability.
		# so let's just put it off.

		if self._controller.MenuIsOpen():
			self._controller.CallLaterWXSafe( self, 0.5, self._SwapMediaPanel, new_panel )

			return

		self._preview_panel.SetMedia( None )

		self._media_panel.ClearPageKey()

		self.ReplaceWindow( self._media_panel, new_panel )

		self._media_panel.DestroyLater()

		self._media_panel = new_panel

		self._controller.pub( 'refresh_page_name', self._page_key )

	def CheckAbleToClose( self ):
		self._management_panel.CheckAbleToClose()

	def CleanBeforeDestroy( self ):
		self._management_panel.CleanBeforeDestroy()

		self._preview_panel.CleanBeforeDestroy()

		self._controller.ReleasePageKey( self._page_key )

	def EventPreviewUnsplit( self, event ):
		self._search_preview_split.Unsplit( self._preview_panel )

		self._controller.pub( 'set_focus', self._page_key, None )

	def EventUnsplit( self, event ):
		self.Unsplit( self._search_preview_split )

		self._controller.pub( 'set_focus', self._page_key, None )

	def GetHashes( self ):
		if self._initialised:
			return self._media_panel.GetHashes( ordered = True )

		else:
			return self._initial_hashes

	def GetManagementController( self ):
		return self._management_controller

	def GetManagementPanel( self ):
		return self._management_panel

	# used by autocomplete

	def GetMedia( self ):
		return self._media_panel.GetSortedMedia()

	def GetMediaPanel( self ):
		return self._media_panel

	def GetName( self ):
		return self._management_controller.GetPageName()

	def GetNumFileSummary( self ):
		if self._initialised:
			num_files = self._media_panel.GetNumFiles()

		else:
			num_files = len( self._initial_hashes )

		( num_value, num_range ) = self._management_controller.GetValueRange()

		if num_value == num_range:
			( num_value, num_range ) = ( 0, 0 )

		return ( num_files, ( num_value, num_range ) )

	def GetPageKey( self ):
		return self._page_key

	def GetPageKeys( self ):
		return { self._page_key }

	def GetPrettyStatus( self ):
		return self._pretty_status

	def GetSashPositions( self ):
		if self.IsSplit():
			x = self.GetSashPosition()

		else:
			x = HC.options[ 'hpos' ]

		if self._search_preview_split.IsSplit():
			# I used to do:
			# y = -1 * self._preview_panel.GetSize()[1]
			# but that crept 4 pixels smaller every time, I assume due to sash caret height

			( sps_x, sps_y ) = self._search_preview_split.GetClientSize()

			sash_y = self._search_preview_split.GetSashPosition()

			y = -1 * ( sps_y - sash_y )

		else:
			y = HC.options[ 'vpos' ]

		return ( x, y )

	def IsMultipleWatcherPage( self ):
		return self._management_controller.GetType() == MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER

	def IsImporter( self ):
		return self._management_controller.IsImporter()

	def IsURLImportPage( self ):
		return self._management_controller.GetType() == MANAGEMENT_TYPE_IMPORT_URLS

	def PageHidden( self ):
		self._management_panel.PageHidden()
		self._media_panel.PageHidden()

	def PageShown( self ):
		self._management_panel.PageShown()
		self._media_panel.PageShown()

	def PrepareToHide( self ):
		self._controller.pub( 'set_focus', self._page_key, None )

	def RefreshQuery( self ):
		if self._initialised:
			self._controller.pub( 'refresh_query', self._page_key )

	def ShowHideSplit( self ):
		if self.IsSplit():
			self.Unsplit( self._search_preview_split )

			self._controller.pub( 'set_focus', self._page_key, None )

		else:
			self.SplitVertically( self._search_preview_split, self._media_panel, HC.options[ 'hpos' ] )

			self._search_preview_split.SplitHorizontally( self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )

	def SetMediaFocus( self ):
		self._media_panel.SetFocus()

	def SetMediaResults( self, media_results ):
		file_service_key = self._management_controller.GetKey( 'file_service' )

		media_panel = MediaPanelThumbnails( self, self._page_key, file_service_key, media_results )

		self._SwapMediaPanel( media_panel )

		self._initialised = True
		self._initial_hashes = []

		wx.CallAfter( self._management_panel.Start ) # importand this is callafter, so it happens after a heavy session load is done

	def SetName( self, name ):
		return self._management_controller.SetPageName( name )

	def SetPrettyStatus( self, page_key, status ):
		if page_key == self._page_key:
			if self._initialised:
				self._SetPrettyStatus( status )

	def SetSearchFocus( self ):
		self._management_panel.SetSearchFocus()

	def SetSplitterPositions( self, hpos, vpos ):
		if self._search_preview_split.IsSplit():
			self._search_preview_split.SetSashPosition( vpos )

		else:
			self._search_preview_split.SplitHorizontally( self._management_panel, self._preview_panel, vpos )

		if self.IsSplit():
			self.SetSashPosition( hpos )

		else:
			self.SplitVertically( self._search_preview_split, self._media_panel, hpos )

		if HC.options[ 'hide_preview' ]:
			wx.CallAfter( self._search_preview_split.Unsplit, self._preview_panel )

	def SetSynchronisedWait( self ):
		self._controller.pub( 'synchronised_wait_switch', self._page_key )

	def Start( self ):
		if self._initial_hashes is not None and len( self._initial_hashes ) > 0:
			self._controller.CallToThread( self.THREADLoadInitialMediaResults, self._controller, self._initial_hashes )

		else:
			self._initialised = True

			wx.CallAfter( self._management_panel.Start ) # importand this is callafter, so it happens after a heavy session load is done

	def SwapMediaPanel( self, new_panel ):
		self._SwapMediaPanel( new_panel )

	def TestAbleToClose( self ):
		try:
			self._management_panel.CheckAbleToClose()

		except VetoException as e:
			reason = str( e )

			with DialogYesNo( self, reason + ' Are you sure you want to close it?' ) as dlg:
				if dlg.ShowModal() == wx.ID_NO:
					raise VetoException()

	def THREADLoadInitialMediaResults( self, controller, initial_hashes ):
		def wx_code_status( status ):
			if not self:
				return

			self._SetPrettyStatus( status )

		def wx_code_publish( media_results ):
			if not self:
				return

			self.SetMediaResults( media_results )

		initial_media_results = []

		for group_of_initial_hashes in SplitListIntoChunks( initial_hashes, 256 ):
			more_media_results = controller.Read( 'media_results', group_of_initial_hashes )

			initial_media_results.extend( more_media_results )

			status = 'Loading initial files\u2026 ' + ConvertValueRangeToPrettyString( len( initial_media_results ), len( initial_hashes ) )

			wx.CallAfter( wx_code_status, status )

		hashes_to_media_results = { media_result.GetHash() : media_result for media_result in initial_media_results }

		sorted_initial_media_results = [ hashes_to_media_results[ hash ] for hash in initial_hashes ]

		wx.CallAfter( wx_code_publish, sorted_initial_media_results )

	def REPEATINGPageUpdate( self ):
		self._management_panel.REPEATINGPageUpdate()

class PagesNotebook( wx.Notebook ):
	def __init__( self, parent, controller, name ):
		if controller.new_options.GetBoolean( 'notebook_tabs_on_left' ):
			style = wx.NB_LEFT

		else:
			style = wx.NB_TOP

		wx.Notebook.__init__( self, parent, style = style )

		self._controller = controller

		self._page_key = self._controller.AcquirePageKey()

		self._name = name

		self._next_new_page_index = None

		self._potential_drag_page = None

		self._closed_pages = []

		self._last_last_session_hash = None

		self._controller.sub( self, 'RefreshPageName', 'refresh_page_name' )
		self._controller.sub( self, 'NotifyPageUnclosed', 'notify_page_unclosed' )

		self.Bind( wx.EVT_MOTION, self.EventDrag )
		self.Bind( wx.EVT_LEFT_DOWN, self.EventLeftDown )
		self.Bind( wx.EVT_LEFT_UP, self.EventLeftUp )
		self.Bind( wx.EVT_LEFT_DCLICK, self.EventLeftDoubleClick )
		self.Bind( wx.EVT_MIDDLE_DOWN, self.EventMiddleClick )
		self.Bind( wx.EVT_RIGHT_DOWN, self.EventMenu )
		self.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.EventPageChanged )

	def _ChooseNewPage( self, insertion_index = None ):
		self._next_new_page_index = insertion_index

		with DialogPageChooser( self, self._controller ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				( page_type, page_data ) = dlg.GetValue()

				if page_type == 'pages':
					self.NewPagesNotebook()

				elif page_type == 'page':
					management_controller = page_data

					self.NewPage( management_controller )

	def _CloseAllPages( self, polite = True, delete_pages = False ):
		closees = [ index for index in range( self.GetPageCount() ) ]

		self._ClosePages( closees, polite, delete_pages = delete_pages )

	def _CloseLeftPages( self, from_index ):
		message = 'Close all pages to the left?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				closees = [ index for index in range( self.GetPageCount() ) if index < from_index ]

				self._ClosePages( closees )

	def _CloseOtherPages( self, except_index ):
		message = 'Close all other pages?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				closees = [ index for index in range( self.GetPageCount() ) if index != except_index ]

				self._ClosePages( closees )

	def _ClosePage( self, index, polite = True, delete_page = False ):
		self._controller.ResetIdleTimer()
		self._controller.ResetPageChangeTimer()

		if index == -1 or index > self.GetPageCount() - 1:
			return False

		page = self.GetPage( index )

		if polite:
			try:
				page.TestAbleToClose()

			except VetoException:
				return False

		page.PrepareToHide()

		page_key = page.GetPageKey()

		self._closed_pages.append( ( index, page_key ) )

		self.RemovePage( index )

		self._controller.pub( 'refresh_page_name', self._page_key )

		if delete_page:
			self._controller.pub( 'notify_deleted_page', page )

		else:
			self._controller.pub( 'notify_closed_page', page )
			self._controller.pub( 'notify_new_undo' )

		return True

	def _ClosePages( self, indices, polite = True, delete_pages = False ):
		indices = list( indices )

		indices.sort( reverse = True ) # so we are closing from the end first

		for index in indices:
			successful = self._ClosePage( index, polite, delete_page = delete_pages )

			if not successful:
				break

	def _CloseRightPages( self, from_index ):
		message = 'Close all pages to the right?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				closees = [ index for index in range( self.GetPageCount() ) if index > from_index ]

				self._ClosePages( closees )

	def _GetDefaultPageInsertionIndex( self ):
		new_options = self._controller.new_options

		new_page_goes = new_options.GetInteger( 'default_new_page_goes' )

		current_index = self.GetSelection()

		if current_index == wx.NOT_FOUND:
			new_page_goes = CC.NEW_PAGE_GOES_FAR_LEFT

		if new_page_goes == CC.NEW_PAGE_GOES_FAR_LEFT:
			insertion_index = 0

		elif new_page_goes == CC.NEW_PAGE_GOES_LEFT_OF_CURRENT:
			insertion_index = current_index

		elif new_page_goes == CC.NEW_PAGE_GOES_RIGHT_OF_CURRENT:
			insertion_index = current_index + 1

		elif new_page_goes == CC.NEW_PAGE_GOES_FAR_RIGHT:
			insertion_index = self.GetPageCount()

		return insertion_index

	def _GetMediaPages( self, only_my_level ):
		results = []

		for page in self._GetPages():
			if isinstance( page, PagesNotebook ):
				if not only_my_level:
					results.extend( page.GetMediaPages() )

			else:
				results.append( page )

		return results

	def _GetIndex( self, page_key ):
		for ( page, index ) in ( ( self.GetPage( index ), index ) for index in range( self.GetPageCount() ) ):
			if page.GetPageKey() == page_key:
				return index

		raise DataMissing()

	def _GetNotebookFromScreenPosition( self, screen_position ):
		current_page = self.GetCurrentPage()

		if current_page is None or not isinstance( current_page, PagesNotebook ):
			return self

		else:
			( tab_index, flags ) = NotebookScreenToHitTest( self, screen_position )

			if tab_index != wx.NOT_FOUND:
				return self

			if HC.PLATFORM_OSX:
				( x, y ) = screen_position
				( child_x, child_y ) = current_page.GetPosition()

				on_child_notebook_somewhere = y > child_y # wew lad, OSX not delivering onpage maybe?

			else:
				on_child_notebook_somewhere = flags & wx.NB_HITTEST_ONPAGE

			if on_child_notebook_somewhere:
				return current_page._GetNotebookFromScreenPosition( screen_position )

		return self

	def _GetPages( self ):
		return [ self.GetPage( i ) for i in range( self.GetPageCount() ) ]

	def _GetPageFromPageKey( self, page_key ):
		for page in self._GetPages():
			if page.GetPageKey() == page_key:
				return page

			if isinstance( page, PagesNotebook ):
				if page.HasPageKey( page_key ):
					return page._GetPageFromPageKey( page_key )

		return None

	def _GetPageFromName( self, page_name ):
		for page in self._GetPages():
			if page.GetName() == page_name:
				return page

			if isinstance( page, PagesNotebook ):
				result = page._GetPageFromName( page_name )

				if result is not None:
					return result

		return None

	def _GetTopNotebook( self ):
		top_notebook = self

		parent = top_notebook.GetParent()

		while isinstance( parent, PagesNotebook ):
			top_notebook = parent

			parent = top_notebook.GetParent()

		return top_notebook

	def _MovePage( self, page, dest_notebook, insertion_tab_index, follow_dropped_page = False ):
		source_notebook = page.GetParent()

		for ( index, p ) in enumerate( source_notebook._GetPages() ):
			if p == page:
				source_notebook.RemovePage( index )

				break

		if source_notebook != dest_notebook:
			page.Reparent( dest_notebook )

			self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )

		insertion_tab_index = min( insertion_tab_index, dest_notebook.GetPageCount() )

		dest_notebook.InsertPage( insertion_tab_index, page, page.GetName(), select = follow_dropped_page )

		if follow_dropped_page:
			self.ShowPage( page )

		self._controller.pub( 'refresh_page_name', page.GetPageKey() )

	def _MovePages( self, pages, dest_notebook ):
		insertion_tab_index = dest_notebook.GetNumPages( only_my_level = True )

		for page in pages:
			if page.GetParent() != dest_notebook:
				self._MovePage( page, dest_notebook, insertion_tab_index )

				insertion_tab_index += 1

	def _ShiftPage( self, page_index, delta = None, new_index = None ):
		new_page_index = page_index

		if delta is not None:
			new_page_index = page_index + delta

		if new_index is not None:
			new_page_index = new_index

		if new_page_index == page_index:
			return

		if 0 <= new_page_index and new_page_index <= self.GetPageCount() - 1:
			page_is_selected = self.GetSelection() == page_index

			page = self.GetPage( page_index )
			name = self.GetPageText( page_index )

			self.RemovePage( page_index )

			self.InsertPage( new_page_index, page, name, page_is_selected )

	def _RefreshPageName( self, index ):
		if index == -1 or index > self.GetPageCount() - 1:
			return

		new_options = self._controller.new_options

		max_page_name_chars = new_options.GetInteger( 'max_page_name_chars' )

		page_file_count_display = new_options.GetInteger( 'page_file_count_display' )

		import_page_progress_display = new_options.GetBoolean( 'import_page_progress_display' )

		page = self.GetPage( index )

		page_name = page.GetName()

		page_name = page_name.replace( os.linesep, '' )

		if len( page_name ) > max_page_name_chars:
			page_name = page_name[ : max_page_name_chars ] + '\u2026'

		num_string = ''

		( num_files, ( num_value, num_range ) ) = page.GetNumFileSummary()

		if page_file_count_display == CC.PAGE_FILE_COUNT_DISPLAY_ALL or ( page_file_count_display == CC.PAGE_FILE_COUNT_DISPLAY_ONLY_IMPORTERS and page.IsImporter() ):
			num_string += ToHumanInt( num_files )

		if import_page_progress_display:
			if num_range > 0 and num_value != num_range:
				if len( num_string ) > 0:
					num_string += ', '

				num_string += ConvertValueRangeToPrettyString( num_value, num_range )

		if len( num_string ) > 0:
			page_name += ' (' + num_string + ')'

		safe_page_name = self.EscapeMnemonics( page_name )

		existing_page_name = self.GetPageText( index )

		if existing_page_name not in ( safe_page_name, page_name ):
			self.SetPageText( index, safe_page_name )

	def _RenamePage( self, index ):
		if index == -1 or index > self.GetPageCount() - 1:
			return

		page = self.GetPage( index )

		current_name = page.GetName()

		with DialogTextEntry( self, 'Enter the new name.', default = current_name, allow_blank = False ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				new_name = dlg.GetValue()

				page.SetName( new_name )

				self._controller.pub( 'refresh_page_name', page.GetPageKey() )

	def _SendPageToNewNotebook( self, index ):
		if 0 <= index and index <= self.GetPageCount() - 1:
			page = self.GetPage( index )

			dest_notebook = self.NewPagesNotebook( forced_insertion_index = index, give_it_a_blank_page = False )

			self._MovePage( page, dest_notebook, 0 )

	def _SendRightPagesToNewNotebook( self, from_index ):
		message = 'Send all pages to the right to a new page of pages?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				pages_index = self.GetPageCount()

				dest_notebook = self.NewPagesNotebook( forced_insertion_index = pages_index, give_it_a_blank_page = False )

				movees = list( range( from_index + 1, pages_index ) )

				movees.reverse()

				for index in movees:
					page = self.GetPage( index )

					self._MovePage( page, dest_notebook, 0 )

	def _ShowMenu( self, screen_position ):
		( tab_index, flags ) = NotebookScreenToHitTest( self, screen_position )

		num_pages = self.GetPageCount()

		end_index = num_pages - 1

		more_than_one_tab = num_pages > 1

		click_over_tab = tab_index != -1

		can_go_left = tab_index > 0
		can_go_right = tab_index < end_index

		click_over_page_of_pages = False

		menu = wx.Menu()

		if click_over_tab:
			page = self.GetPage( tab_index )

			click_over_page_of_pages = isinstance( page, PagesNotebook )

			AppendMenuItem( self, menu, 'close page', 'Close this page.', self._ClosePage, tab_index )

			if num_pages > 1:
				AppendMenuItem( self, menu, 'close other pages', 'Close all pages but this one.', self._CloseOtherPages, tab_index )

				if can_go_left:
					AppendMenuItem( self, menu, 'close pages to the left', 'Close all pages to the left of this one.', self._CloseLeftPages, tab_index )

				if can_go_right:
					AppendMenuItem( self, menu, 'close pages to the right', 'Close all pages to the right of this one.', self._CloseRightPages, tab_index )

			AppendSeparator( menu )

			AppendMenuItem( self, menu, 'rename page', 'Rename this page.', self._RenamePage, tab_index )

		AppendMenuItem( self, menu, 'new page', 'Choose a new page.', self._ChooseNewPage )

		if click_over_tab:
			AppendMenuItem( self, menu, 'new page here', 'Choose a new page.', self._ChooseNewPage, tab_index )

			if more_than_one_tab:
				AppendSeparator( menu )

				can_home = tab_index > 1
				can_move_left = tab_index > 0
				can_move_right = tab_index < end_index
				can_end = tab_index < end_index - 1

				if can_home:
					AppendMenuItem( self, menu, 'move to left end', 'Move this page all the way to the left.', self._ShiftPage, tab_index, new_index = 0 )

				if can_move_left:
					AppendMenuItem( self, menu, 'move left', 'Move this page one to the left.', self._ShiftPage, tab_index, delta = -1 )

				if can_move_right:
					AppendMenuItem( self, menu, 'move right', 'Move this page one to the right.', self._ShiftPage, tab_index, 1 )

				if can_end:
					AppendMenuItem( self, menu, 'move to right end', 'Move this page all the way to the right.', self._ShiftPage, tab_index, new_index = end_index )

			AppendSeparator( menu )

			AppendMenuItem( self, menu, 'send this page down to a new page of pages', 'Make a new page of pages and put this page in it.', self._SendPageToNewNotebook, tab_index )

			if can_go_right:
				AppendMenuItem( self, menu, 'send pages to the right to a new page of pages', 'Make a new page of pages and put all the pages to the right into it.', self._SendRightPagesToNewNotebook, tab_index )

			if click_over_page_of_pages and page.GetPageCount() > 0:
				AppendSeparator( menu )

				AppendMenuItem( self, menu, 'refresh all this page\'s pages', 'Command every page below this one to refresh.', page.RefreshAllPages )

		existing_session_names = self._controller.Read( 'serialisable_names', HG.SerialisableType["GUI_SESSION"] )

		if len( existing_session_names ) > 0 or click_over_page_of_pages:
			AppendSeparator( menu )

		if len( existing_session_names ) > 0:
			submenu = wx.Menu()

			for name in existing_session_names:
				AppendMenuItem( self, submenu, name, 'Load this session here.', self.AppendGUISession, name )

			AppendMenu( menu, submenu, 'append session' )

		if click_over_page_of_pages:
			submenu = wx.Menu()

			for name in existing_session_names:
				if name in RESERVED_SESSION_NAMES:
					continue

				AppendMenuItem( self, submenu, name, 'Save this page of pages to the session.', page.SaveGUISession, name )

			AppendMenuItem( self, submenu, 'create a new session', 'Save this page of pages to the session.', page.SaveGUISession, suggested_name = page.GetName() )

			AppendMenu( menu, submenu, 'save this page of pages to a session' )

		self._controller.PopupMenu( self, menu )

	def AppendGUISession( self, name, load_in_a_page_of_pages = True ):
		try:
			session = self._controller.Read( 'serialisable_named', HG.SerialisableType["GUI_SESSION"], name )

		except Exception as e:
			ShowText( 'While trying to load session ' + name + ', this error happened:' )
			ShowException( e )

			self.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY )

			return

		if load_in_a_page_of_pages:
			destination = self.NewPagesNotebook( name = name, give_it_a_blank_page = False)

		else:
			destination = self

		page_tuples = session.GetPageTuples()

		destination.AppendSessionPageTuples( page_tuples )

	def AppendGUISessionBackup( self, name, timestamp, load_in_a_page_of_pages = True ):
		try:
			session = self._controller.Read( 'serialisable_named', HG.SerialisableType["GUI_SESSION"], name, timestamp )

		except Exception as e:
			ShowText( 'While trying to load session ' + name + ' (ts ' + str( timestamp ) + ', this error happened:' )
			ShowException( e )

			self.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY )

			return

		if load_in_a_page_of_pages:
			destination = self.NewPagesNotebook( name = name, give_it_a_blank_page = False)

		else:
			destination = self

		page_tuples = session.GetPageTuples()

		destination.AppendSessionPageTuples( page_tuples )

	def AppendSessionPageTuples( self, page_tuples ):
		starting_index = self._GetDefaultPageInsertionIndex()

		forced_insertion_index = starting_index

		done_first_page = False

		for page_tuple in page_tuples:
			( page_type, page_data ) = page_tuple

			if page_type == 'pages':
				( name, subpage_tuples ) = page_data

				try:
					page = self.NewPagesNotebook( name, forced_insertion_index = forced_insertion_index, give_it_a_blank_page = False, select_page = False )

					page.AppendSessionPageTuples( subpage_tuples )

				except Exception as e:
					ShowException( e )

			elif page_type == 'page':
				( management_controller, initial_hashes ) = page_data

				try:
					select_page = not done_first_page

					self.NewPage( management_controller, initial_hashes = initial_hashes, forced_insertion_index = forced_insertion_index, select_page = select_page )

					done_first_page = True

				except Exception as e:
					ShowException( e )

			forced_insertion_index += 1

	def ChooseNewPage( self ):
		self._ChooseNewPage()

	def ChooseNewPageForDeepestNotebook( self ):
		current_page = self.GetCurrentPage()

		if isinstance( current_page, PagesNotebook ):
			current_page.ChooseNewPageForDeepestNotebook()

		else:
			self._ChooseNewPage()

	def CleanBeforeDestroy( self ):
		for page in self._GetPages():
			page.CleanBeforeDestroy()

		self._controller.ReleasePageKey( self._page_key )

	def CloseCurrentPage( self, polite = True ):
		selection = self.GetSelection()

		if selection != wx.NOT_FOUND:
			page = self.GetPage( selection )

			if isinstance( page, PagesNotebook ):
				if page.GetNumPages() > 0:
					page.CloseCurrentPage( polite )

				else:
					self._ClosePage( selection, polite = polite )

			else:
				self._ClosePage( selection, polite = polite )

	def EventDrag( self, event ):
		if event.Dragging() and self._potential_drag_page is not None:
			drop_source = wx.DropSource( self._controller.gui )

			#

			hydrus_page_tab_data_object = wx.CustomDataObject( 'application/hydrus-page-tab' )

			data = self._potential_drag_page.GetPageKey()

			hydrus_page_tab_data_object.SetData( data )

			#

			drop_source.SetData( hydrus_page_tab_data_object )

			drop_source.DoDragDrop( wx.Drag_DefaultMove )

			self._potential_drag_page = None

		event.Skip()

	def EventLeftDown( self, event ):
		event_skip_ok = True

		position = event.GetPosition()

		( tab_index, flags ) = self.HitTest( position )

		if tab_index != -1:
			page = self.GetPage( tab_index )

			if HC.PLATFORM_OSX and page == self.GetCurrentPage():
				# drag doesn't work if we allow the event to go ahead
				# but we do want the event to go ahead if it is a 'select different page' event

				event_skip_ok = False

			self._potential_drag_page = page

		if event_skip_ok:
			event.Skip()

	def EventLeftDoubleClick( self, event ):
		position = event.GetPosition()

		( tab_index, flags ) = self.HitTest( position )

		if tab_index == wx.NOT_FOUND:
			if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE:
				screen_position = ClientToScreen( self, position )

				notebook = self._GetNotebookFromScreenPosition( screen_position )

				notebook.EventNewPageFromScreenPosition( screen_position )

			else:
				self.ChooseNewPage()

		else:
			event.Skip()

	def EventLeftUp( self, event ):
		self._potential_drag_page = None

		event.Skip()

	def EventMenu( self, event ):
		screen_position = ClientToScreen( self, event.GetPosition() )

		self._ShowMenu( screen_position )

	def EventMenuFromScreenPosition( self, position ):
		notebook = self._GetNotebookFromScreenPosition( position )

		notebook._ShowMenu( position )

	def EventMiddleClick( self, event ):
		if self._controller.MenuIsOpen():
			return

		position = event.GetPosition()

		( tab_index, flags ) = self.HitTest( position )

		if tab_index == wx.NOT_FOUND:
			if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE:
				screen_position = ClientToScreen( self, position )

				notebook = self._GetNotebookFromScreenPosition( screen_position )

				notebook.EventNewPageFromScreenPosition( screen_position )

			else:
				self.ChooseNewPage()

		else:
			self._ClosePage( tab_index )

	def EventNewPageFromScreenPosition( self, position ):
		notebook = self._GetNotebookFromScreenPosition( position )

		notebook._ChooseNewPage()

	def EventPageChanged( self, event ):
		if event.EventObject == self: # because OS X wants to bump this up to parent notebooks

			old_selection = event.GetOldSelection()
			selection = event.GetSelection()

			if old_selection != wx.NOT_FOUND:
				self.GetPage( old_selection ).PageHidden()

			if selection != wx.NOT_FOUND:
				self.GetPage( selection ).PageShown()

			self._controller.gui.RefreshStatusBar()

		if HC.PLATFORM_OSX:
			event.Skip() # need this or OS X spergs out and never .Show()s new page, wew

		self._controller.pub( 'notify_page_change' )

	def GetCurrentMediaPage( self ):
		page = self.GetCurrentPage()

		if isinstance( page, PagesNotebook ):
			return page.GetCurrentMediaPage()

		else:
			return page # this can be None

	def GetMediaPages( self, only_my_level = False ):
		return self._GetMediaPages( only_my_level )

	def GetName( self ):
		return self._name

	def GetNumFileSummary( self ):
		total_num_files = 0
		total_num_value = 0
		total_num_range = 0

		for page in self._GetPages():
			( num_files, ( num_value, num_range ) ) = page.GetNumFileSummary()

			total_num_files += num_files
			total_num_value += num_value
			total_num_range += num_range

		return ( total_num_files, ( total_num_value, total_num_range ) )

	def GetNumPages( self, only_my_level = False ):
		if only_my_level:
			return self.GetPageCount()

		else:
			total = 0

			for page in self._GetPages():
				if isinstance( page, PagesNotebook ):
					total += page.GetNumPages( False )

				else:
					total += 1

			return total

	def GetOrMakeMultipleWatcherPage( self, desired_page_name = None, select_page = True ):
		potential_watcher_pages = [ page for page in self._GetMediaPages( False ) if page.IsMultipleWatcherPage() ]

		if desired_page_name is not None:
			potential_watcher_pages = [ page for page in potential_watcher_pages if page.GetName() == desired_page_name ]

		if len( potential_watcher_pages ) > 0:
			# ok, we can use an existing one. should we use the current?

			current_media_page = self.GetCurrentMediaPage()

			if current_media_page is not None and current_media_page in potential_watcher_pages:
				return current_media_page

			else:
				return potential_watcher_pages[0]

		else:
			return self.NewPageImportMultipleWatcher( page_name = desired_page_name, on_deepest_notebook = True, select_page = select_page )

	def GetOrMakeURLImportPage( self, desired_page_name = None, select_page = True ):
		potential_url_import_pages = [ page for page in self._GetMediaPages( False ) if page.IsURLImportPage() ]

		if desired_page_name is not None:
			potential_url_import_pages = [ page for page in potential_url_import_pages if page.GetName() == desired_page_name ]

		if len( potential_url_import_pages ) > 0:
			# ok, we can use an existing one. should we use the current?

			current_media_page = self.GetCurrentMediaPage()

			if current_media_page is not None and current_media_page in potential_url_import_pages:
				return current_media_page

			else:
				return potential_url_import_pages[0]

		else:
			return self.NewPageImportURLs( page_name = desired_page_name, on_deepest_notebook = True, select_page = select_page )

	def GetPageKey( self ):
		return self._page_key

	def GetPageKeys( self ):
		page_keys = { self._page_key }

		for page in self._GetPages():
			page_keys.update( page.GetPageKeys() )

		return page_keys

	def GetPages( self ):
		return self._GetPages()

	def GetPrettyStatus( self ):
		( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary()

		num_string = ToHumanInt( num_files )

		if num_range > 0 and num_value != num_range:
			num_string += ', ' + ConvertValueRangeToPrettyString( num_value, num_range )

		return ToHumanInt( self.GetPageCount() ) + ' pages, ' + num_string + ' files'

	def GetTestAbleToCloseStatement( self ):
		count = collections.Counter()

		for page in self._GetMediaPages( False ):
			try:
				page.CheckAbleToClose()

			except VetoException as e:
				reason = str( e )

				count[ reason ] += 1

		if len( count ) > 0:
			# total_problems = sum( count.values() )

			message = ''

			for ( reason, c ) in list( count.items() ):
				if c == 1:
					message = '1 page says: ' + reason

				else:
					message = ToHumanInt( c ) + ' pages say:' + reason

				message += os.linesep

			return message

		else:
			return None

	def HasMediaPageName( self, page_name, only_my_level = False ):
		media_pages = self._GetMediaPages( only_my_level )

		for page in media_pages:
			if page.GetName() == page_name:
				return True

		return False

	def HasPage( self, page ):
		return self.HasPageKey( page.GetPageKey() )

	def HasPageKey( self, page_key ):
		for page in self._GetPages():
			if page.GetPageKey() == page_key:
				return True

			elif isinstance( page, PagesNotebook ) and page.HasPageKey( page_key ):
				return True

		return False

	def HasMultipleWatcherPage( self ):
		for page in self._GetPages():
			if isinstance( page, PagesNotebook ):
				if page.HasMultipleWatcherPage():
					return True

			else:
				if page.IsMultipleWatcherPage():
					return True

		return False

	def HasURLImportPage( self ):
		for page in self._GetPages():
			if isinstance( page, PagesNotebook ):
				if page.HasURLImportPage():
					return True

			else:
				if page.IsURLImportPage():
					return True

		return False

	def IsMultipleWatcherPage( self ):
		return False

	def IsImporter( self ):
		return False

	def IsURLImportPage( self ):
		return False

	def LoadGUISession( self, name ):
		if self.GetPageCount() > 0:
			message = 'Close the current pages and load session "' + name + '"?'

			with DialogYesNo( self, message, title = 'Clear and load session?' ) as yn_dlg:
				if yn_dlg.ShowModal() != wx.ID_YES:
					return

			try:
				self.TestAbleToClose()

			except VetoException:
				return

			self._CloseAllPages( polite = False, delete_pages = True )

			self._controller.CallLaterWXSafe( self, 1.0, self.AppendGUISession, name, load_in_a_page_of_pages = False )

		else:
			self.AppendGUISession( name, load_in_a_page_of_pages = False )

	def MediaDragAndDropDropped( self, source_page_key, hashes ):
		source_page = self._GetPageFromPageKey( source_page_key )

		if source_page is None:
			return

		screen_position = wx.GetMousePosition()

		dest_notebook = self._GetNotebookFromScreenPosition( screen_position )

		( x, y ) = screen_position

		( tab_index, flags ) = NotebookScreenToHitTest( dest_notebook, ( x, y ) )

		do_add = True
		# do chase - if we need to chase to an existing dest page on which we dropped files
		# do return - if we need to return to source page if we created a new one

		if flags & wx.NB_HITTEST_ONPAGE:
			dest_page = dest_notebook.GetCurrentPage()

		elif tab_index == wx.NOT_FOUND:
			dest_page = dest_notebook.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = hashes )

			do_add = False

		else:
			dest_page = dest_notebook.GetPage( tab_index )

			if isinstance( dest_page, PagesNotebook ):
				result = dest_page.GetCurrentMediaPage()

				if result is None:
					dest_page = dest_page.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = hashes )

					do_add = False

				else:
					dest_page = result

		if dest_page is None:
			return # we somehow dropped onto a new notebook that has no pages

		if dest_page.GetPageKey() == source_page_key:
			return # we dropped onto the same page we picked up on

		if do_add:
			media_results = self._controller.Read( 'media_results', hashes, sorted = True )

			dest_page.GetMediaPanel().AddMediaResults( dest_page.GetPageKey(), media_results )

		else:
			self.ShowPage( source_page )

		ctrl_down = wx.GetKeyState( wx.WXK_COMMAND ) or wx.GetKeyState( wx.WXK_CONTROL )

		if not ctrl_down:
			source_page.GetMediaPanel().RemoveMedia( source_page.GetPageKey(), hashes )

	def NewPage( self, management_controller, initial_hashes = None, forced_insertion_index = None, on_deepest_notebook = False, select_page = True ):
		current_page = self.GetCurrentPage()

		if on_deepest_notebook and isinstance( current_page, PagesNotebook ):
			return current_page.NewPage( management_controller, initial_hashes = initial_hashes, forced_insertion_index = forced_insertion_index, on_deepest_notebook = on_deepest_notebook )

		WARNING_TOTAL_PAGES = self._controller.new_options.GetInteger( 'total_pages_warning' )
		MAX_TOTAL_PAGES = 200

		( total_active_page_count, total_closed_page_count ) = self._controller.gui.GetTotalPageCounts()

		if total_active_page_count + total_closed_page_count >= WARNING_TOTAL_PAGES:
			self._controller.gui.DeleteAllClosedPages()

		if not HG.no_page_limit_mode:
			if total_active_page_count >= MAX_TOTAL_PAGES:
				message = 'The client should not have more than ' + str( MAX_TOTAL_PAGES ) + ' pages open, as it leads to program instability! Are you sure you want to open more pages?'

				with DialogYesNo( self, message, title = 'Too many pages!', yes_label = 'yes, and do not tell me again', no_label = 'no' ) as dlg:
					if dlg.ShowModal() == wx.ID_YES:
						HG.no_page_limit_mode = True

						self._controller.pub( 'notify_new_options' )

					else:
						return

			if total_active_page_count == WARNING_TOTAL_PAGES:
				ShowText( 'You have ' + str( total_active_page_count ) + ' pages open! You can only open a few more before program stability is affected! Please close some now!' )

		self._controller.ResetIdleTimer()
		self._controller.ResetPageChangeTimer()

		if initial_hashes is None:
			initial_hashes = []

		page = Page( self, self._controller, management_controller, initial_hashes )

		if forced_insertion_index is None:
			if self._next_new_page_index is None:
				insertion_index = self._GetDefaultPageInsertionIndex()

			else:
				insertion_index = self._next_new_page_index

				self._next_new_page_index = None

		else:
			insertion_index = forced_insertion_index

		page_name = page.GetName()

		# in some unusual circumstances, this gets out of whack
		insertion_index = min( insertion_index, self.GetPageCount() )

		self.InsertPage( insertion_index, page, page_name, select = select_page )

		self._controller.pub( 'refresh_page_name', page.GetPageKey() )
		self._controller.pub( 'notify_new_pages' )

		wx.CallAfter( page.Start )

		if select_page:
			page.SetSearchFocus()

			# this is here for now due to the pagechooser having a double-layer dialog on a booru choice, which messes up some focus inheritance

			self._controller.CallLaterWXSafe( self, 0.5, page.SetSearchFocus )

		return page

	def NewPageDuplicateFilter( self, on_deepest_notebook = False ):
		management_controller = CreateManagementControllerDuplicateFilter()

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )

	def NewPageImportGallery( self, on_deepest_notebook = False ):
		management_controller = CreateManagementControllerImportGallery()

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )

	def NewPageImportSimpleDownloader( self, on_deepest_notebook = False ):
		management_controller = CreateManagementControllerImportSimpleDownloader()

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )

	def NewPageImportMultipleWatcher( self, page_name = None, url = None, on_deepest_notebook = False, select_page = True ):
		management_controller = CreateManagementControllerImportMultipleWatcher( page_name = page_name, url = url )

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook, select_page = select_page )

	def NewPageImportURLs( self, page_name = None, on_deepest_notebook = False, select_page = True ):
		management_controller = CreateManagementControllerImportURLs( page_name = page_name )

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook, select_page = select_page )

	def NewPagePetitions( self, service_key, on_deepest_notebook = False ):
		management_controller = CreateManagementControllerPetitions( service_key )

		return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )

	def NewPageQuery( self, file_service_key, initial_hashes = None, initial_predicates = None, page_name = None, on_deepest_notebook = False, do_sort = False, select_page = True ):
		if initial_hashes is None:
			initial_hashes = []

		if initial_predicates is None:
			initial_predicates = []

		if page_name is None:
			page_name = 'files'

		search_enabled = len( initial_hashes ) == 0

		new_options = self._controller.new_options

		tag_service_key = new_options.GetKey( 'default_tag_service_search_page' )

		if not self._controller.services_manager.ServiceExists( tag_service_key ):
			tag_service_key = CC.COMBINED_TAG_SERVICE_KEY

		file_search_context = FileSearchContext( file_service_key = file_service_key, tag_service_key = tag_service_key, predicates = initial_predicates )

		management_controller = CreateManagementControllerQuery( page_name, file_service_key, file_search_context, search_enabled )

		page = self.NewPage( management_controller, initial_hashes = initial_hashes, on_deepest_notebook = on_deepest_notebook, select_page = select_page )

		if do_sort:
			HG.client_controller.pub( 'do_page_sort', page.GetPageKey() )

		return page

	def NewPagesNotebook( self, name = 'pages', forced_insertion_index = None, on_deepest_notebook = False, give_it_a_blank_page = True, select_page = True ):
		current_page = self.GetCurrentPage()

		if on_deepest_notebook and isinstance( current_page, PagesNotebook ):
			return current_page.NewPagesNotebook( name = name, forced_insertion_index = forced_insertion_index, on_deepest_notebook = on_deepest_notebook, give_it_a_blank_page = give_it_a_blank_page )

		self._controller.ResetIdleTimer()
		self._controller.ResetPageChangeTimer()

		page = PagesNotebook( self, self._controller, name )

		if forced_insertion_index is None:
			if self._next_new_page_index is None:
				insertion_index = self._GetDefaultPageInsertionIndex()

			else:
				insertion_index = self._next_new_page_index

				self._next_new_page_index = None

		else:
			insertion_index = forced_insertion_index

		page_name = page.GetName()

		self.InsertPage( insertion_index, page, page_name, select = select_page )

		self._controller.pub( 'refresh_page_name', page.GetPageKey() )

		if give_it_a_blank_page:
			page.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY )

		return page

	def NotifyPageUnclosed( self, page ):
		page_key = page.GetPageKey()

		for ( index, closed_page_key ) in self._closed_pages:
			if page_key == closed_page_key:
				page.Show()

				insert_index = min( index, self.GetPageCount() )

				name = page.GetName()

				self.InsertPage( insert_index, page, name, True )

				self._controller.pub( 'refresh_page_name', page.GetPageKey() )

				self._closed_pages.remove( ( index, closed_page_key ) )

				break

	def PageHidden( self ):
		result = self.GetCurrentPage()

		if result is not None:
			result.PageHidden()

	def PageShown( self ):
		result = self.GetCurrentPage()

		if result is not None:
			result.PageShown()

	def PageDragAndDropDropped( self, page_key ):
		page = self._GetPageFromPageKey( page_key )

		if page is None:
			return

		screen_position = wx.GetMousePosition()

		dest_notebook = self._GetNotebookFromScreenPosition( screen_position )

		( x, y ) = screen_position

		( tab_index, flags ) = NotebookScreenToHitTest( dest_notebook, ( x, y ) )

		if flags & wx.NB_HITTEST_ONPAGE:
			# was not dropped on label area, so ditch DnD

			return

		if tab_index == wx.NOT_FOUND:
			# if it isn't dropped on anything, put it on the end

			tab_index = dest_notebook.GetPageCount()

			if tab_index > 0:
				if dest_notebook.GetPage( tab_index - 1 ) == page:
					return

		else:
			EDGE_PADDING = 10

			( left_tab_index, gumpf ) = NotebookScreenToHitTest( dest_notebook, ( x - EDGE_PADDING, y ) )
			( right_tab_index, gumpf ) = NotebookScreenToHitTest( dest_notebook,  ( x + EDGE_PADDING, y ) )

			landed_near_left_edge = left_tab_index != tab_index
			landed_near_right_edge = right_tab_index != tab_index

			landed_on_edge = landed_near_right_edge or landed_near_left_edge
			landed_in_middle = not landed_on_edge

			# there_is_a_page_to_the_left = tab_index > 0
			there_is_a_page_to_the_right = tab_index < dest_notebook.GetPageCount() - 1

			# page_on_left_is_source = there_is_a_page_to_the_left and dest_notebook.GetPage( tab_index - 1 ) == page
			page_on_right_is_source = there_is_a_page_to_the_right and dest_notebook.GetPage( tab_index + 1 ) == page

			# dropped on source and not on the right edge: do nothing

			landee_page = dest_notebook.GetPage( tab_index )

			if landee_page == page:
				if landed_near_right_edge and there_is_a_page_to_the_right:
					tab_index += 1

				else:
					return

			# dropped just to the left of source: do nothing

			if landed_near_right_edge and page_on_right_is_source:
				return

			# dropped on left side of an edge: insert on right side

			if landed_near_right_edge:
				tab_index += 1

			if landed_in_middle and isinstance( landee_page, PagesNotebook ):
				dest_notebook = landee_page
				tab_index = dest_notebook.GetPageCount()

		if dest_notebook == page or IsWXAncestor( dest_notebook, page ):
			# can't drop a notebook beneath itself!
			return

		insertion_tab_index = tab_index

		shift_down = wx.GetKeyState( wx.WXK_SHIFT )

		follow_dropped_page = not shift_down

		new_options = HG.client_controller.new_options

		if new_options.GetBoolean( 'reverse_page_shift_drag_behaviour' ):
			follow_dropped_page = not follow_dropped_page

		self._MovePage( page, dest_notebook, insertion_tab_index, follow_dropped_page )

		self.Refresh()

		if dest_notebook != self:
			dest_notebook.Refresh()

	def PrepareToHide( self ):
		for page in self._GetPages():
			page.PrepareToHide()

	def PresentImportedFilesToPage( self, hashes, page_name ):
		page = self._GetPageFromName( page_name )

		if page is None:
			page = self.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = hashes, page_name = page_name, on_deepest_notebook = True, select_page = False )

		else:
			media_results = self._controller.Read( 'media_results', hashes, sorted = True )
			page.GetMediaPanel().AddMediaResults( page.GetPageKey(), media_results )

		return page

	def RefreshAllPages( self ):
		for page in self._GetPages():
			if isinstance( page, PagesNotebook ):
				page.RefreshAllPages()

			else:
				page.RefreshQuery()

	def RefreshPageName( self, page_key = None ):
		if page_key is None:
			for index in range( self.GetPageCount() ):
				self._RefreshPageName( index )

		else:
			for ( index, page ) in enumerate( self._GetPages() ):
				do_it = False

				if page.GetPageKey() == page_key:
					do_it = True

				elif isinstance( page, PagesNotebook ) and page.HasPageKey( page_key ):
					do_it = True

				if do_it:
					self._RefreshPageName( index )

					break

	def SaveGUISession( self, name = None, suggested_name = '' ):
		if name is None:
			while True:
				with DialogTextEntry( self, 'Enter a name for the new session.', default = suggested_name ) as dlg:
					if dlg.ShowModal() == wx.ID_OK:
						name = dlg.GetValue()

						if name in RESERVED_SESSION_NAMES:
							wx.MessageBox( 'Sorry, you cannot have that name! Try another.' )

						else:
							existing_session_names = self._controller.Read( 'serialisable_names', HG.SerialisableType["GUI_SESSION"] )

							if name in existing_session_names:
								message = 'Session \'' + name + '\' already exists! Do you want to overwrite it?'

								with DialogYesNo( self, message, title = 'Overwrite existing session?', yes_label = 'yes, overwrite', no_label = 'no, choose another name' ) as yn_dlg:
									if yn_dlg.ShowModal() != wx.ID_YES:
										continue

							break

					else:
						return

		elif name not in RESERVED_SESSION_NAMES: # i.e. a human asked to do this

			message = 'Overwrite this session?'

			with DialogYesNo( self, message, title = 'Overwrite existing session?', yes_label = 'yes, overwrite', no_label = 'no' ) as yn_dlg:
				if yn_dlg.ShowModal() != wx.ID_YES:
					return

		#

		session = GUISession( name )

		for page in self._GetPages():
			session.AddPageTuple( page )

		#

		if name == 'last session':
			session_hash = hashlib.sha256( bytes( session.DumpToString(), 'utf-8' ) ).digest()

			if session_hash == self._last_last_session_hash:
				return

			self._last_last_session_hash = session_hash

		self._controller.WriteSynchronous( 'serialisable', session )

		self._controller.pub( 'notify_new_sessions' )

	def SetName( self, name ):
		self._name = name

	def ShowPage( self, showee ):
		for ( i, page ) in enumerate( self._GetPages() ):
			if isinstance( page, wx.Notebook ) and page.HasPage( showee ):
				self.SetSelection( i )

				page.ShowPage( showee )

				break

			elif page == showee:
				self.SetSelection( i )

				break

	def TestAbleToClose( self ):
		statement = self.GetTestAbleToCloseStatement()

		if statement is not None:
			message = 'Are you sure you want to close this page of pages?'
			message += os.linesep * 2
			message += statement

			with DialogYesNo( self, message ) as dlg:
				if dlg.ShowModal() == wx.ID_NO:
					raise VetoException()

	def REPEATINGPageUpdate( self ):
		pass

class GUISession( SerialisableBaseNamed ):
	SERIALISABLE_TYPE_NUM = HG.SerialisableType["GUI_SESSION"]
	SERIALISABLE_NAME = 'GUI Session'
	SERIALISABLE_VERSION = 4

	def __init__( self, name ):
		SerialisableBaseNamed.__init__( self, name )

		self._page_tuples = []

	def _GetPageTuple( self, page ):
		if isinstance( page, PagesNotebook ):
			name = page.GetName()

			page_tuples = [ self._GetPageTuple( subpage ) for subpage in page.GetPages() ]

			return ( 'pages', ( name, page_tuples ) )

		else:
			management_controller = page.GetManagementController()

			hashes = list( page.GetHashes() )

			return ( 'page', ( management_controller, hashes ) )

	def _GetSerialisableInfo( self ):
		def handle_e( e ):
			ShowText( 'Attempting to save a page to the session failed! Its data tuple and error follows! Please close it or see if you can clear any potentially invalid data from it!' )

			ShowText( page_tuple )

			ShowException( e )

		def GetSerialisablePageTuple( page_tuple ):
			( page_type, page_data ) = page_tuple

			if page_type == 'pages':
				( name, page_tuples ) = page_data

				serialisable_page_tuples = []

				for pt in page_tuples:
					try:
						serialisable_page_tuples.append( GetSerialisablePageTuple( pt ) )

					except Exception as e:
						handle_e( e )

				serialisable_page_data = ( name, serialisable_page_tuples )

			elif page_type == 'page':
				( management_controller, hashes ) = page_data

				serialisable_management_controller = management_controller.GetSerialisableTuple()

				serialisable_hashes = [ hash.hex() for hash in hashes ]

				serialisable_page_data = ( serialisable_management_controller, serialisable_hashes )

			serialisable_tuple = ( page_type, serialisable_page_data )

			return serialisable_tuple

		serialisable_info = []

		for page_tuple in self._page_tuples:
			try:
				serialisable_page_tuple = GetSerialisablePageTuple( page_tuple )

				serialisable_info.append( serialisable_page_tuple )

			except Exception as e:
				handle_e( e )

		return serialisable_info

	def _InitialiseFromSerialisableInfo( self, serialisable_info ):
		def handle_e( e ):
			ShowText( 'A page failed to load! Its serialised data and error follows!' )

			ShowText( serialisable_page_tuple )

			ShowException( e )

		def GetPageTuple( serialisable_page_tuple ):
			( page_type, serialisable_page_data ) = serialisable_page_tuple

			if page_type == 'pages':
				( name, serialisable_page_tuples ) = serialisable_page_data

				page_tuples = []

				for spt in serialisable_page_tuples:
					try:
						page_tuples.append( GetPageTuple( spt ) )

					except Exception as e:
						handle_e( e )

				page_data = ( name, page_tuples )

			elif page_type == 'page':
				( serialisable_management_controller, serialisable_hashes ) = serialisable_page_data

				management_controller = CreateFromSerialisableTuple( serialisable_management_controller )

				hashes = [ bytes.fromhex( hash ) for hash in serialisable_hashes ]

				page_data = ( management_controller, hashes )

			page_tuple = ( page_type, page_data )

			return page_tuple

		for serialisable_page_tuple in serialisable_info:
			try:
				page_tuple = GetPageTuple( serialisable_page_tuple )

				self._page_tuples.append( page_tuple )

			except Exception as e:
				handle_e( e )

	def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
		if version == 1:
			new_serialisable_info = []

			for ( page_name, serialisable_management_controller, serialisable_hashes ) in old_serialisable_info:
				management_controller = CreateFromSerialisableTuple( serialisable_management_controller )

				management_controller.SetPageName( page_name )

				serialisable_management_controller = management_controller.GetSerialisableTuple()

				new_serialisable_info.append( ( serialisable_management_controller, serialisable_hashes ) )

			return ( 2, new_serialisable_info )

		if version == 2:
			new_serialisable_info = []

			for ( serialisable_management_controller, serialisable_hashes ) in old_serialisable_info:
				new_serialisable_info.append( ( 'page', ( serialisable_management_controller, serialisable_hashes ) ) )

			return ( 3, new_serialisable_info )

		if version == 3:
			def clean_tuple( spt ):
				( page_type, serialisable_page_data ) = spt

				if page_type == 'pages':
					( name, pages_serialisable_page_tuples ) = serialisable_page_data

					if name.startswith( '[USER]' ) and len( name ) > 6:
						name = name[6:]

					pages_serialisable_page_tuples = [ clean_tuple( pages_spt ) for pages_spt in pages_serialisable_page_tuples ]

					return ( 'pages', ( name, pages_serialisable_page_tuples ) )

				else:
					return spt

			new_serialisable_info = []

			serialisable_page_tuples = old_serialisable_info

			for serialisable_page_tuple in serialisable_page_tuples:
				serialisable_page_tuple = clean_tuple( serialisable_page_tuple )

				new_serialisable_info.append( serialisable_page_tuple )

			return ( 4, new_serialisable_info )

	def AddPageTuple( self, page ):
		page_tuple = self._GetPageTuple( page )

		self._page_tuples.append( page_tuple )

	def GetPageTuples( self ):
		return self._page_tuples

HG.LookupTables[ "SERIALISABLE_TYPES_TO_OBJECT_TYPES" ][ HG.SerialisableType["GUI_SESSION"] ] = GUISession