hydrus/include/ClientGUIACDropdown.py

1276 lines
45 KiB
Python
Raw Normal View History

2016-09-21 19:54:04 +00:00
import ClientCaches
import ClientConstants as CC
import ClientGUICommon
import ClientSearch
import collections
import HydrusConstants as HC
import HydrusExceptions
import HydrusGlobals
import HydrusTags
import itertools
import wx
ID_TIMER_DROPDOWN_HIDE = wx.NewId()
ID_TIMER_AC_LAG = wx.NewId()
# much of this is based on the excellent TexCtrlAutoComplete class by Edward Flick, Michele Petrazzo and Will Sadkin, just with plenty of simplification and integration into hydrus
class AutoCompleteDropdown( wx.Panel ):
def __init__( self, parent ):
wx.Panel.__init__( self, parent )
self._intercept_key_events = True
self._last_search_text = ''
self._next_updatelist_is_probably_fast = False
tlp = self.GetTopLevelParent()
# There's a big bug in wx where FRAME_FLOAT_ON_PARENT Frames don't get passed their mouse events if their parent is a Dialog jej
# I think it is something to do with the initialisation order; if the frame is init'ed before the ShowModal call, but whatever.
# This turned out to be ugly when I added the manage tags frame, so I've set it to if the tlp has a parent, which basically means "not the main gui"
if tlp.GetParent() is not None or HC.options[ 'always_embed_autocompletes' ]:
self._float_mode = False
else:
self._float_mode = True
self._text_ctrl = wx.TextCtrl( self, style=wx.TE_PROCESS_ENTER )
self._text_ctrl.SetBackgroundColour( wx.Colour( *HC.options[ 'gui_colours' ][ 'autocomplete_background' ] ) )
self._last_attempted_dropdown_width = 0
self._last_attempted_dropdown_position = ( None, None )
if self._float_mode:
self._text_ctrl.Bind( wx.EVT_SET_FOCUS, self.EventSetFocus )
self._text_ctrl.Bind( wx.EVT_KILL_FOCUS, self.EventKillFocus )
self._text_ctrl.Bind( wx.EVT_TEXT, self.EventText )
self._text_ctrl.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
self._text_ctrl.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._text_ctrl, CC.FLAGS_EXPAND_PERPENDICULAR )
#self._dropdown_window = wx.PopupWindow( self, flags = wx.BORDER_RAISED )
#self._dropdown_window = wx.PopupTransientWindow( self, style = wx.BORDER_RAISED )
#self._dropdown_window = wx.Window( self, style = wx.BORDER_RAISED )
#self._dropdown_window = wx.Panel( self )
if self._float_mode:
self._dropdown_window = wx.Frame( self, style = wx.FRAME_TOOL_WINDOW | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.BORDER_RAISED )
self._dropdown_window.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )
self._dropdown_window.SetSize( ( 0, 0 ) )
self._dropdown_window.SetPosition( self._text_ctrl.ClientToScreenXY( 0, 0 ) )
self._dropdown_window.Show()
self._dropdown_window.Bind( wx.EVT_CLOSE, self.EventCloseDropdown )
self._dropdown_hidden = True
self._list_height = 250
else:
self._dropdown_window = wx.Panel( self )
self._list_height = 125
self._dropdown_list = self._InitDropDownList()
if not self._float_mode: vbox.AddF( self._dropdown_window, CC.FLAGS_EXPAND_BOTH_WAYS )
self.SetSizer( vbox )
self._cache_text = ''
self._cached_results = []
self._initial_matches_fetched = False
if self._float_mode:
self.Bind( wx.EVT_MOVE, self.EventMove )
self.Bind( wx.EVT_SIZE, self.EventMove )
self.Bind( wx.EVT_TIMER, self.TIMEREventDropdownHide, id = ID_TIMER_DROPDOWN_HIDE )
self._move_hide_timer = wx.Timer( self, id = ID_TIMER_DROPDOWN_HIDE )
self._move_hide_timer.Start( 1, wx.TIMER_ONE_SHOT )
tlp.Bind( wx.EVT_MOVE, self.EventMove )
parent = self
while True:
try:
parent = parent.GetParent()
if isinstance( parent, wx.ScrolledWindow ):
parent.Bind( wx.EVT_SCROLLWIN, self.EventMove )
except:
break
self.Bind( wx.EVT_TIMER, self.TIMEREventLag, id = ID_TIMER_AC_LAG )
self._lag_timer = wx.Timer( self, id = ID_TIMER_AC_LAG )
wx.CallAfter( self._UpdateList )
def _BroadcastChoices( self, predicates ):
raise NotImplementedError()
def _BroadcastCurrentText( self ):
text = self._text_ctrl.GetValue()
self._BroadcastChoices( { text } )
def _GenerateMatches( self ):
raise NotImplementedError()
def _HideDropdown( self ):
if not self._dropdown_hidden:
self._dropdown_window.SetSize( ( 0, 0 ) )
self._dropdown_hidden = True
def _InitDropDownList( self ):
raise NotImplementedError()
def _ShouldShow( self ):
tlp_active = self.GetTopLevelParent().IsActive() or self._dropdown_window.IsActive()
if HC.PLATFORM_LINUX:
tlp = self.GetTopLevelParent()
if isinstance( tlp, wx.Dialog ):
visible = True
else:
# notebook on linux doesn't 'hide' things apparently, so isshownonscreen, which recursively tests parents' hide status, doesn't work!
gui = HydrusGlobals.client_controller.GetGUI()
current_page = gui.GetCurrentPage()
visible = ClientGUICommon.IsWXAncestor( self, current_page )
else:
visible = self._text_ctrl.IsShownOnScreen()
focus_window = wx.Window.FindFocus()
focus_remains_on_self_or_children = focus_window == self._dropdown_window or focus_window in self._dropdown_window.GetChildren() or focus_window == self._text_ctrl
return tlp_active and visible and focus_remains_on_self_or_children
def _ShouldTakeResponsibilityForEnter( self ):
raise NotImplementedError()
def _ShowDropdown( self ):
( text_width, text_height ) = self._text_ctrl.GetSize()
desired_dropdown_position = self._text_ctrl.ClientToScreenXY( -2, text_height - 2 )
if self._last_attempted_dropdown_position != desired_dropdown_position:
self._dropdown_window.SetPosition( desired_dropdown_position )
self._last_attempted_dropdown_position = desired_dropdown_position
#
show_and_fit_needed = False
if self._dropdown_hidden:
show_and_fit_needed = True
else:
if text_width != self._last_attempted_dropdown_width:
show_and_fit_needed = True
if show_and_fit_needed:
self._dropdown_window.Fit()
self._dropdown_window.SetSize( ( text_width, -1 ) )
self._dropdown_window.Layout()
self._dropdown_hidden = False
self._last_attempted_dropdown_width = text_width
def _TakeResponsibilityForEnter( self ):
raise NotImplementedError()
def _UpdateList( self ):
pass
def BroadcastChoices( self, predicates ):
self._BroadcastChoices( predicates )
def EventCloseDropdown( self, event ):
HydrusGlobals.client_controller.GetGUI().Close()
def EventKeyDown( self, event ):
HydrusGlobals.client_controller.ResetIdleTimer()
if event.KeyCode in ( wx.WXK_INSERT, wx.WXK_NUMPAD_INSERT ):
if self._intercept_key_events:
self._intercept_key_events = False
( r, g, b ) = HC.options[ 'gui_colours' ][ 'autocomplete_background' ]
if r != g or r != b or g != b:
colour = wx.Colour( g, b, r )
elif r > 127:
colour = wx.Colour( g, b, r / 2 )
else:
colour = wx.Colour( g, b, r * 2 )
else:
self._intercept_key_events = True
colour = wx.Colour( *HC.options[ 'gui_colours' ][ 'autocomplete_background' ] )
self._text_ctrl.SetBackgroundColour( colour )
self._text_ctrl.Refresh()
elif event.KeyCode == wx.WXK_SPACE and event.RawControlDown(): # this is control, not command on os x, for which command+space does some os stuff
self._UpdateList()
self._lag_timer.Stop()
elif self._intercept_key_events:
if event.KeyCode in ( ord( 'A' ), ord( 'a' ) ) and event.CmdDown():
event.Skip()
elif event.KeyCode in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ) and self._ShouldTakeResponsibilityForEnter():
self._TakeResponsibilityForEnter()
elif event.KeyCode == wx.WXK_ESCAPE:
self.GetTopLevelParent().SetFocus()
elif event.KeyCode in ( wx.WXK_UP, wx.WXK_NUMPAD_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ) and self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
if event.KeyCode in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ): id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select_up' )
elif event.KeyCode in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ): id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select_down' )
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
self._text_ctrl.ProcessEvent( new_event )
elif event.KeyCode in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
if event.KeyCode in ( wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ):
id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'canvas_show_previous' )
elif event.KeyCode in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN ):
id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'canvas_show_next' )
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
self._text_ctrl.ProcessEvent( new_event )
else:
self._dropdown_list.ProcessEvent( event )
else:
event.Skip()
def EventKillFocus( self, event ):
self._move_hide_timer.Start( 1, wx.TIMER_ONE_SHOT )
event.Skip()
def EventMouseWheel( self, event ):
if self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
if event.GetWheelRotation() > 0: id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select_up' )
else: id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'select_down' )
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
self.ProcessEvent( new_event )
else:
if event.CmdDown():
key_event = wx.KeyEvent( wx.EVT_KEY_DOWN.typeId )
if event.GetWheelRotation() > 0: key_event.m_keyCode = wx.WXK_UP
else: key_event.m_keyCode = wx.WXK_DOWN
self._dropdown_list.ProcessEvent( key_event )
else:
# for some reason, the scrolledwindow list doesn't process scroll events properly when in a popupwindow
# so let's just tell it to scroll manually
( start_x, start_y ) = self._dropdown_list.GetViewStart()
if event.GetWheelRotation() > 0: self._dropdown_list.Scroll( -1, start_y - 3 )
else: self._dropdown_list.Scroll( -1, start_y + 3 )
if event.GetWheelRotation() > 0: command_type = wx.wxEVT_SCROLLWIN_LINEUP
else: command_type = wx.wxEVT_SCROLLWIN_LINEDOWN
wx.PostEvent( self, wx.ScrollWinEvent( command_type ) )
def EventMove( self, event ):
try:
self._HideDropdown()
self._move_hide_timer.Start( 250, wx.TIMER_ONE_SHOT )
except wx.PyDeadObjectError: pass
event.Skip()
def EventSetFocus( self, event ):
self._move_hide_timer.Start( 1, wx.TIMER_ONE_SHOT )
event.Skip()
def EventText( self, event ):
num_chars = len( self._text_ctrl.GetValue() )
if num_chars == 0:
self._UpdateList()
elif HC.options[ 'fetch_ac_results_automatically' ]:
( char_limit, long_wait, short_wait ) = HC.options[ 'ac_timings' ]
self._next_updatelist_is_probably_fast = self._next_updatelist_is_probably_fast and num_chars > len( self._last_search_text )
if self._next_updatelist_is_probably_fast: self._UpdateList()
elif num_chars < char_limit: self._lag_timer.Start( long_wait, wx.TIMER_ONE_SHOT )
else: self._lag_timer.Start( short_wait, wx.TIMER_ONE_SHOT )
def RefreshList( self ):
self._cache_text = ''
self._current_namespace = ''
self._UpdateList()
def TIMEREventDropdownHide( self, event ):
try:
should_show = self._ShouldShow()
if should_show:
self._ShowDropdown()
else:
self._HideDropdown()
self._move_hide_timer.Start( 250, wx.TIMER_ONE_SHOT )
except wx.PyDeadObjectError:
self._move_hide_timer.Stop()
except:
self._move_hide_timer.Stop()
raise
def TIMEREventLag( self, event ):
try:
self._UpdateList()
except wx.PyDeadObjectError:
self._lag_timer.Stop()
except:
self._lag_timer.Stop()
raise
class AutoCompleteDropdownTags( AutoCompleteDropdown ):
def __init__( self, parent, file_service_key, tag_service_key ):
self._file_service_key = file_service_key
self._tag_service_key = tag_service_key
AutoCompleteDropdown.__init__( self, parent )
self._current_namespace = ''
self._current_matches = []
file_service = HydrusGlobals.client_controller.GetServicesManager().GetService( self._file_service_key )
tag_service = HydrusGlobals.client_controller.GetServicesManager().GetService( self._tag_service_key )
2016-10-26 20:45:34 +00:00
self._file_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, file_service.GetName(), self.FileButtonHit )
2016-09-21 19:54:04 +00:00
self._file_repo_button.SetMinSize( ( 20, -1 ) )
2016-10-26 20:45:34 +00:00
self._tag_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, tag_service.GetName(), self.TagButtonHit )
2016-09-21 19:54:04 +00:00
self._tag_repo_button.SetMinSize( ( 20, -1 ) )
self.Bind( wx.EVT_MENU, self.EventMenu )
def _ChangeFileService( self, file_service_key ):
if file_service_key == CC.COMBINED_FILE_SERVICE_KEY and self._tag_service_key == CC.COMBINED_TAG_SERVICE_KEY:
self._ChangeTagService( CC.LOCAL_TAG_SERVICE_KEY )
self._file_service_key = file_service_key
file_service = HydrusGlobals.client_controller.GetServicesManager().GetService( self._file_service_key )
name = file_service.GetName()
self._file_repo_button.SetLabelText( name )
wx.CallAfter( self.RefreshList )
def _ChangeTagService( self, tag_service_key ):
if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY and self._file_service_key == CC.COMBINED_FILE_SERVICE_KEY:
self._ChangeFileService( CC.LOCAL_FILE_SERVICE_KEY )
self._tag_service_key = tag_service_key
self._dropdown_list.SetTagService( self._tag_service_key )
tag_service = tag_service = HydrusGlobals.client_controller.GetServicesManager().GetService( self._tag_service_key )
name = tag_service.GetName()
self._tag_repo_button.SetLabelText( name )
self._cache_text = ''
self._current_namespace = ''
wx.CallAfter( self.RefreshList )
def _UpdateList( self ):
self._last_search_text = self._text_ctrl.GetValue()
matches = self._GenerateMatches()
self._initial_matches_fetched = True
self._dropdown_list.SetPredicates( matches )
self._current_matches = matches
num_chars = len( self._text_ctrl.GetValue() )
if num_chars == 0:
self._lag_timer.Start( 5 * 60 * 1000, wx.TIMER_ONE_SHOT )
def EventMenu( self, event ):
action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
if action is not None:
( command, data ) = action
if command == 'change_file_service':
self._ChangeFileService( data )
elif command == 'change_tag_service':
self._ChangeTagService( data )
else:
event.Skip()
return # this is about select_up and select_down
2016-10-26 20:45:34 +00:00
def FileButtonHit( self ):
services_manager = HydrusGlobals.client_controller.GetServicesManager()
services = []
services.append( services_manager.GetService( CC.COMBINED_FILE_SERVICE_KEY ) )
services.append( services_manager.GetService( CC.LOCAL_FILE_SERVICE_KEY ) )
services.append( services_manager.GetService( CC.TRASH_SERVICE_KEY ) )
services.extend( services_manager.GetServices( ( HC.FILE_REPOSITORY, ) ) )
menu = wx.Menu()
for service in services: menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'change_file_service', service.GetServiceKey() ), service.GetName() )
HydrusGlobals.client_controller.PopupMenu( self._file_repo_button, menu )
def SetFileService( self, file_service_key ):
self._ChangeFileService( file_service_key )
def SetTagService( self, tag_service_key ):
self._ChangeTagService( tag_service_key )
def TagButtonHit( self ):
2016-09-21 19:54:04 +00:00
services_manager = HydrusGlobals.client_controller.GetServicesManager()
services = []
services.append( services_manager.GetService( CC.COMBINED_TAG_SERVICE_KEY ) )
services.append( services_manager.GetService( CC.LOCAL_TAG_SERVICE_KEY ) )
services.extend( services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) )
menu = wx.Menu()
for service in services: menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'change_tag_service', service.GetServiceKey() ), service.GetName() )
HydrusGlobals.client_controller.PopupMenu( self._tag_repo_button, menu )
class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
def __init__( self, parent, page_key, file_search_context, media_callable = None, synchronised = True, include_unusual_predicate_types = True ):
file_service_key = file_search_context.GetFileServiceKey()
tag_service_key = file_search_context.GetTagServiceKey()
AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )
self._media_callable = media_callable
self._page_key = page_key
self._file_search_context = file_search_context
self._include_current_tags = ClientGUICommon.OnOffButton( self._dropdown_window, self._page_key, 'notify_include_current', on_label = 'include current tags', off_label = 'exclude current tags', start_on = file_search_context.IncludeCurrentTags() )
self._include_current_tags.SetToolTipString( 'select whether to include current tags in the search' )
self._include_pending_tags = ClientGUICommon.OnOffButton( self._dropdown_window, self._page_key, 'notify_include_pending', on_label = 'include pending tags', off_label = 'exclude pending tags', start_on = file_search_context.IncludePendingTags() )
self._include_pending_tags.SetToolTipString( 'select whether to include pending tags in the search' )
self._synchronised = ClientGUICommon.OnOffButton( self._dropdown_window, self._page_key, 'notify_search_immediately', on_label = 'searching immediately', off_label = 'waiting -- tag counts may be inaccurate', start_on = synchronised )
self._synchronised.SetToolTipString( 'select whether to renew the search as soon as a new predicate is entered' )
self._include_unusual_predicate_types = include_unusual_predicate_types
button_hbox_1 = wx.BoxSizer( wx.HORIZONTAL )
button_hbox_1.AddF( self._include_current_tags, CC.FLAGS_EXPAND_BOTH_WAYS )
button_hbox_1.AddF( self._include_pending_tags, CC.FLAGS_EXPAND_BOTH_WAYS )
button_hbox_2 = wx.BoxSizer( wx.HORIZONTAL )
button_hbox_2.AddF( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
button_hbox_2.AddF( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( button_hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
vbox.AddF( self._synchronised, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( button_hbox_2, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
vbox.AddF( self._dropdown_list, CC.FLAGS_EXPAND_BOTH_WAYS )
self._dropdown_window.SetSizer( vbox )
HydrusGlobals.client_controller.sub( self, 'SetSynchronisedWait', 'synchronised_wait_switch' )
HydrusGlobals.client_controller.sub( self, 'IncludeCurrent', 'notify_include_current' )
HydrusGlobals.client_controller.sub( self, 'IncludePending', 'notify_include_pending' )
def _BroadcastChoices( self, predicates ):
if self._text_ctrl.GetValue() != '':
self._text_ctrl.SetValue( '' )
HydrusGlobals.client_controller.pub( 'enter_predicates', self._page_key, predicates )
def _BroadcastCurrentText( self ):
( inclusive, search_text, entry_predicate ) = self._ParseSearchText()
try:
HydrusTags.CheckTagNotEmpty( search_text )
except HydrusExceptions.SizeException:
return
self._BroadcastChoices( { entry_predicate } )
def _ChangeFileService( self, file_service_key ):
AutoCompleteDropdownTags._ChangeFileService( self, file_service_key )
self._file_search_context.SetFileServiceKey( file_service_key )
HydrusGlobals.client_controller.pub( 'change_file_service', self._page_key, file_service_key )
HydrusGlobals.client_controller.pub( 'refresh_query', self._page_key )
def _ChangeTagService( self, tag_service_key ):
AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )
self._file_search_context.SetTagServiceKey( tag_service_key )
HydrusGlobals.client_controller.pub( 'change_tag_service', self._page_key, tag_service_key )
HydrusGlobals.client_controller.pub( 'refresh_query', self._page_key )
def _InitDropDownList( self ):
return ClientGUICommon.ListBoxTagsAutocompleteDropdownRead( self._dropdown_window, self._tag_service_key, self.BroadcastChoices, min_height = self._list_height )
def _ParseSearchText( self ):
raw_entry = self._text_ctrl.GetValue()
if raw_entry.startswith( '-' ):
inclusive = False
search_text = raw_entry[1:]
else:
inclusive = True
search_text = raw_entry
search_text = HydrusTags.CleanTag( search_text )
siblings_manager = HydrusGlobals.client_controller.GetManager( 'tag_siblings' )
sibling = siblings_manager.GetSibling( self._tag_service_key, search_text )
if sibling is None:
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, search_text, inclusive )
else:
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, sibling, inclusive )
return ( inclusive, search_text, entry_predicate )
def _GenerateMatches( self ):
self._next_updatelist_is_probably_fast = False
num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]
( inclusive, search_text, entry_predicate ) = self._ParseSearchText()
if search_text in ( '', ':' ):
input_just_changed = self._cache_text != ''
db_not_going_to_hang_if_we_hit_it = not HydrusGlobals.client_controller.DBCurrentlyDoingJob()
if input_just_changed or db_not_going_to_hang_if_we_hit_it or not self._initial_matches_fetched:
self._cache_text = ''
self._current_namespace = ''
if self._file_service_key == CC.COMBINED_FILE_SERVICE_KEY: search_service_key = self._tag_service_key
else: search_service_key = self._file_service_key
self._cached_results = HydrusGlobals.client_controller.Read( 'file_system_predicates', search_service_key )
matches = self._cached_results
else:
must_do_a_search = False
if '*' in search_text:
must_do_a_search = True
if ':' in search_text:
( namespace, half_complete_tag ) = search_text.split( ':', 1 )
if namespace != self._current_namespace:
self._current_namespace = namespace # do a new search, no matter what half_complete tag is
if half_complete_tag != '': must_do_a_search = True
else:
if self._cache_text == self._current_namespace + ':' and half_complete_tag != '':
must_do_a_search = True
else:
self._current_namespace = ''
half_complete_tag = search_text
siblings_manager = HydrusGlobals.client_controller.GetManager( 'tag_siblings' )
if half_complete_tag == '':
self._cache_text = self._current_namespace + ':'
matches = [] # a query like 'namespace:'
else:
fetch_from_db = True
if self._media_callable is not None:
media = self._media_callable()
can_fetch_from_media = media is not None and len( media ) > 0
if can_fetch_from_media and self._synchronised.IsOn():
fetch_from_db = False
if fetch_from_db:
include_current = self._file_search_context.IncludeCurrentTags()
include_pending = self._file_search_context.IncludePendingTags()
if len( half_complete_tag ) < num_autocomplete_chars and '*' not in search_text:
predicates = HydrusGlobals.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = search_text, exact_match = True, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = True, collapse_siblings = True )
else:
if must_do_a_search or self._cache_text == '' or not search_text.startswith( self._cache_text ):
self._cache_text = search_text
self._cached_results = HydrusGlobals.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = search_text, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = True, collapse_siblings = True )
predicates = self._cached_results
self._next_updatelist_is_probably_fast = True
else:
# it is possible that media will change between calls to this, so don't cache it
# it's also quick as hell, so who cares
tags_managers = []
for m in media:
if m.IsCollection(): tags_managers.extend( m.GetSingletonsTagsManagers() )
else: tags_managers.append( m.GetTagsManager() )
tags_to_do = set()
current_tags_to_count = collections.Counter()
pending_tags_to_count = collections.Counter()
if self._file_search_context.IncludeCurrentTags():
lists_of_current_tags = [ list( tags_manager.GetCurrent( self._tag_service_key ) ) for tags_manager in tags_managers ]
current_tags_flat_iterable = itertools.chain.from_iterable( lists_of_current_tags )
current_tags_flat = ClientSearch.FilterTagsBySearchEntry( self._tag_service_key, search_text, current_tags_flat_iterable )
current_tags_to_count.update( current_tags_flat )
tags_to_do.update( current_tags_to_count.keys() )
if self._file_search_context.IncludePendingTags():
lists_of_pending_tags = [ list( tags_manager.GetPending( self._tag_service_key ) ) for tags_manager in tags_managers ]
pending_tags_flat_iterable = itertools.chain.from_iterable( lists_of_pending_tags )
pending_tags_flat = ClientSearch.FilterTagsBySearchEntry( self._tag_service_key, search_text, pending_tags_flat_iterable )
pending_tags_to_count.update( pending_tags_flat )
tags_to_do.update( pending_tags_to_count.keys() )
predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag, inclusive, current_tags_to_count[ tag ], pending_tags_to_count[ tag ] ) for tag in tags_to_do ]
if self._tag_service_key != CC.COMBINED_TAG_SERVICE_KEY:
predicates = siblings_manager.CollapsePredicates( self._tag_service_key, predicates )
self._next_updatelist_is_probably_fast = True
matches = ClientSearch.FilterPredicatesBySearchEntry( self._tag_service_key, search_text, predicates )
matches = ClientSearch.SortPredicates( matches )
if self._include_unusual_predicate_types:
if self._current_namespace != '':
if '*' not in self._current_namespace and half_complete_tag == '':
matches.insert( 0, ClientSearch.Predicate( HC.PREDICATE_TYPE_NAMESPACE, self._current_namespace, inclusive ) )
if half_complete_tag != '':
if '*' in self._current_namespace or ( '*' in half_complete_tag and half_complete_tag != '*' ):
matches.insert( 0, ClientSearch.Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive ) )
elif '*' in search_text:
matches.insert( 0, ClientSearch.Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive ) )
for match in matches:
if match.GetInclusive() != inclusive: match.SetInclusive( inclusive )
try:
index = matches.index( entry_predicate )
predicate = matches[ index ]
del matches[ index ]
matches.insert( 0, predicate )
except:
pass
return matches
def _ShouldTakeResponsibilityForEnter( self ):
# when the user has quickly typed something in and the results are not yet in
return self._text_ctrl.GetValue() != '' and self._last_search_text == ''
def _TakeResponsibilityForEnter( self ):
self._BroadcastCurrentText()
def GetFileSearchContext( self ):
return self._file_search_context
def IncludeCurrent( self, page_key, value ):
if page_key == self._page_key:
self._file_search_context.SetIncludeCurrentTags( value )
wx.CallAfter( self.RefreshList )
HydrusGlobals.client_controller.pub( 'refresh_query', self._page_key )
def IncludePending( self, page_key, value ):
if page_key == self._page_key:
self._file_search_context.SetIncludePendingTags( value )
wx.CallAfter( self.RefreshList )
HydrusGlobals.client_controller.pub( 'refresh_query', self._page_key )
def SetSynchronisedWait( self, page_key ):
if page_key == self._page_key: self._synchronised.EventButton( None )
class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
def __init__( self, parent, chosen_tag_callable, expand_parents, file_service_key, tag_service_key, null_entry_callable = None ):
self._chosen_tag_callable = chosen_tag_callable
self._expand_parents = expand_parents
self._null_entry_callable = null_entry_callable
if tag_service_key != CC.COMBINED_TAG_SERVICE_KEY and HC.options[ 'show_all_tags_in_autocomplete' ]:
file_service_key = CC.COMBINED_FILE_SERVICE_KEY
AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )
vbox = wx.BoxSizer( wx.VERTICAL )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
hbox.AddF( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
vbox.AddF( self._dropdown_list, CC.FLAGS_EXPAND_BOTH_WAYS )
self._dropdown_window.SetSizer( vbox )
def _BroadcastChoices( self, predicates ):
if self._text_ctrl.GetValue() != '':
self._text_ctrl.SetValue( '' )
tags = { predicate.GetValue() for predicate in predicates }
if len( tags ) > 0:
self._chosen_tag_callable( tags )
def _ParseSearchText( self ):
raw_entry = self._text_ctrl.GetValue()
search_text = HydrusTags.CleanTag( raw_entry )
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, search_text )
siblings_manager = HydrusGlobals.client_controller.GetManager( 'tag_siblings' )
sibling = siblings_manager.GetSibling( self._tag_service_key, search_text )
if sibling is not None:
sibling_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, sibling )
else:
sibling_predicate = None
return ( search_text, entry_predicate, sibling_predicate )
def _BroadcastCurrentText( self ):
( search_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()
try:
HydrusTags.CheckTagNotEmpty( search_text )
except HydrusExceptions.SizeException:
return
self._BroadcastChoices( { entry_predicate } )
def _GenerateMatches( self ):
self._next_updatelist_is_probably_fast = False
num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]
( search_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()
if search_text in ( '', ':' ):
self._cache_text = ''
self._current_namespace = ''
matches = []
else:
must_do_a_search = False
if ':' in search_text:
( namespace, other_half ) = search_text.split( ':', 1 )
if other_half != '' and namespace != self._current_namespace:
self._current_namespace = namespace # do a new search, no matter what half_complete tag is
must_do_a_search = True
else:
self._current_namespace = ''
half_complete_tag = search_text
if len( half_complete_tag ) < num_autocomplete_chars and '*' not in search_text:
predicates = HydrusGlobals.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = search_text, exact_match = True, add_namespaceless = False, collapse_siblings = False )
else:
if must_do_a_search or self._cache_text == '' or not half_complete_tag.startswith( self._cache_text ):
self._cache_text = half_complete_tag
self._cached_results = HydrusGlobals.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = search_text, add_namespaceless = False, collapse_siblings = False )
predicates = self._cached_results
self._next_updatelist_is_probably_fast = True
matches = ClientSearch.FilterPredicatesBySearchEntry( self._tag_service_key, half_complete_tag, predicates )
matches = ClientSearch.SortPredicates( matches )
self._PutAtTopOfMatches( matches, entry_predicate )
if sibling_predicate is not None:
self._PutAtTopOfMatches( matches, sibling_predicate )
if self._expand_parents:
parents_manager = HydrusGlobals.client_controller.GetManager( 'tag_parents' )
matches = parents_manager.ExpandPredicates( self._tag_service_key, matches )
return matches
def _InitDropDownList( self ):
return ClientGUICommon.ListBoxTagsAutocompleteDropdownWrite( self._dropdown_window, self._tag_service_key, self.BroadcastChoices, min_height = self._list_height )
def _PutAtTopOfMatches( self, matches, predicate ):
try:
index = matches.index( predicate )
predicate = matches[ index ]
matches.remove( predicate )
except ValueError:
pass
matches.insert( 0, predicate )
def _ShouldTakeResponsibilityForEnter( self ):
# when the user has quickly typed something in and the results are not yet in
p1 = self._text_ctrl.GetValue() != '' and self._last_search_text == ''
# when the text ctrl is empty and we want to push a None to the parent dialog
p2 = self._text_ctrl.GetValue() == ''
return p1 or p2
def _TakeResponsibilityForEnter( self ):
if self._text_ctrl.GetValue() == '':
if self._null_entry_callable is not None:
self._null_entry_callable()
else:
self._BroadcastCurrentText()