1278 lines
45 KiB
Python
1278 lines
45 KiB
Python
|
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 )
|
||
|
|
||
|
self._file_repo_button = wx.Button( self._dropdown_window, label = file_service.GetName() )
|
||
|
self._file_repo_button.Bind( wx.EVT_BUTTON, self.EventFileButton )
|
||
|
self._file_repo_button.SetMinSize( ( 20, -1 ) )
|
||
|
|
||
|
self._tag_repo_button = wx.Button( self._dropdown_window, label = tag_service.GetName() )
|
||
|
self._tag_repo_button.Bind( wx.EVT_BUTTON, self.EventTagButton )
|
||
|
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 SetFileService( self, file_service_key ):
|
||
|
|
||
|
self._ChangeFileService( file_service_key )
|
||
|
|
||
|
|
||
|
def SetTagService( self, tag_service_key ):
|
||
|
|
||
|
self._ChangeTagService( tag_service_key )
|
||
|
|
||
|
|
||
|
def EventFileButton( self, event ):
|
||
|
|
||
|
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 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
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
def EventTagButton( self, event ):
|
||
|
|
||
|
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()
|
||
|
|
||
|
|
||
|
|