1334 lines
46 KiB
Python
1334 lines
46 KiB
Python
import ClientCaches
|
|
import ClientConstants as CC
|
|
import ClientData
|
|
import ClientGUICommon
|
|
import ClientGUIListBoxes
|
|
import ClientGUIMenus
|
|
import ClientSearch
|
|
import collections
|
|
import HydrusConstants as HC
|
|
import HydrusData
|
|
import HydrusExceptions
|
|
import HydrusGlobals as HG
|
|
import HydrusTags
|
|
import itertools
|
|
import wx
|
|
|
|
ID_TIMER_DROPDOWN_HIDE = wx.NewId()
|
|
ID_TIMER_AC_LAG = wx.NewId()
|
|
|
|
( SelectUpEvent, EVT_SELECT_UP ) = wx.lib.newevent.NewCommandEvent()
|
|
( SelectDownEvent, EVT_SELECT_DOWN ) = wx.lib.newevent.NewCommandEvent()
|
|
|
|
( ShowPreviousEvent, EVT_SHOW_PREVIOUS ) = wx.lib.newevent.NewCommandEvent()
|
|
( ShowNextEvent, EVT_SHOW_NEXT ) = wx.lib.newevent.NewCommandEvent()
|
|
|
|
# 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"
|
|
|
|
not_main_gui = tlp.GetParent() is not None
|
|
|
|
if not_main_gui 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._UpdateBackgroundColour()
|
|
|
|
self._last_attempted_dropdown_width = 0
|
|
self._last_attempted_dropdown_position = ( None, None )
|
|
|
|
self._last_move_event_started = 0.0
|
|
self._last_move_event_occurred = 0.0
|
|
|
|
if self._float_mode:
|
|
|
|
self._text_ctrl.Bind( wx.EVT_SET_FOCUS, self.EventSetFocus )
|
|
self._text_ctrl.Bind( wx.EVT_KILL_FOCUS, self.EventKillFocus )
|
|
|
|
|
|
self._text_ctrl.Bind( wx.EVT_TEXT, self.EventText )
|
|
self._text_ctrl.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
|
|
|
|
self._text_ctrl.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.Add( self._text_ctrl, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#self._dropdown_window = wx.PopupWindow( self, flags = wx.BORDER_RAISED )
|
|
#self._dropdown_window = wx.PopupTransientWindow( self, style = wx.BORDER_RAISED )
|
|
#self._dropdown_window = wx.Window( self, style = wx.BORDER_RAISED )
|
|
|
|
#self._dropdown_window = wx.Panel( self )
|
|
|
|
if self._float_mode:
|
|
|
|
self._dropdown_window = wx.Frame( self, style = wx.FRAME_TOOL_WINDOW | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.BORDER_RAISED )
|
|
|
|
self._dropdown_window.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )
|
|
|
|
self._dropdown_window.SetSize( ( 0, 0 ) )
|
|
|
|
if self._text_ctrl.IsShown():
|
|
|
|
self._dropdown_window.SetPosition( self._text_ctrl.ClientToScreen( ( 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.Add( self._dropdown_window, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
self._cache_text = None
|
|
self._cached_results = []
|
|
|
|
self._initial_matches_fetched = False
|
|
|
|
self._move_hide_job = None
|
|
|
|
if self._float_mode:
|
|
|
|
self.Bind( wx.EVT_MOVE, self.EventMove )
|
|
self.Bind( wx.EVT_SIZE, self.EventMove )
|
|
|
|
HG.client_controller.sub( self, '_ParentMovedOrResized', 'main_gui_move_event' )
|
|
|
|
parent = self
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
parent = parent.GetParent()
|
|
|
|
if isinstance( parent, wx.ScrolledWindow ):
|
|
|
|
parent.Bind( wx.EVT_SCROLLWIN, self.EventMove )
|
|
|
|
|
|
except:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
|
|
|
|
self._refresh_list_job = None
|
|
|
|
self._ScheduleListRefresh( 0.0 )
|
|
|
|
|
|
def _BroadcastChoices( self, predicates ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def _BroadcastCurrentText( self ):
|
|
|
|
text = self._text_ctrl.GetValue()
|
|
|
|
self._BroadcastChoices( { text } )
|
|
|
|
|
|
def _CancelScheduledListRefresh( self ):
|
|
|
|
if self._refresh_list_job is not None:
|
|
|
|
self._refresh_list_job.Cancel()
|
|
|
|
|
|
|
|
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 _ParentMovedOrResized( self ):
|
|
|
|
if self._float_mode:
|
|
|
|
if HydrusData.TimeHasPassedFloat( self._last_move_event_occurred + 1.0 ):
|
|
|
|
self._last_move_event_started = HydrusData.GetNowFloat()
|
|
|
|
|
|
self._last_move_event_occurred = HydrusData.GetNowFloat()
|
|
|
|
# we'll do smoother move updates for a little bit to stop flickeryness, but after that we'll just hide
|
|
|
|
NICE_ANIMATION_GRACE_PERIOD = 0.25
|
|
|
|
time_to_delay_these_calls = HydrusData.TimeHasPassedFloat( self._last_move_event_started + NICE_ANIMATION_GRACE_PERIOD )
|
|
|
|
if time_to_delay_these_calls:
|
|
|
|
self._HideDropdown()
|
|
|
|
if self._ShouldShow():
|
|
|
|
if self._move_hide_job is None:
|
|
|
|
self._move_hide_job = HG.client_controller.CallRepeatingWXSafe( self._dropdown_window, 0.25, 0.0, self.DropdownHideShow )
|
|
|
|
|
|
self._move_hide_job.Delay( 0.25 )
|
|
|
|
|
|
else:
|
|
|
|
self.DropdownHideShow()
|
|
|
|
|
|
|
|
|
|
def _ScheduleListRefresh( self, delay ):
|
|
|
|
if self._refresh_list_job is not None and delay == 0.0:
|
|
|
|
self._refresh_list_job.MoveNextWorkTimeToNow()
|
|
|
|
else:
|
|
|
|
self._CancelScheduledListRefresh()
|
|
|
|
self._refresh_list_job = HG.client_controller.CallLaterWXSafe( self, delay, self._UpdateList )
|
|
|
|
|
|
|
|
def _SetListDirty( self ):
|
|
|
|
self._cache_text = None
|
|
|
|
self._ScheduleListRefresh( 0.0 )
|
|
|
|
|
|
def _ShouldShow( self ):
|
|
|
|
tlp_active = self.GetTopLevelParent().IsActive() or self._dropdown_window.IsActive()
|
|
|
|
if HC.PLATFORM_LINUX:
|
|
|
|
tlp = self.GetTopLevelParent()
|
|
|
|
if isinstance( tlp, wx.Dialog ):
|
|
|
|
visible = True
|
|
|
|
else:
|
|
|
|
# notebook on linux doesn't 'hide' things apparently, so isshownonscreen, which recursively tests parents' hide status, doesn't work!
|
|
|
|
gui = HG.client_controller.GetGUI()
|
|
|
|
current_page = gui.GetCurrentPage()
|
|
|
|
visible = ClientGUICommon.IsWXAncestor( self, current_page )
|
|
|
|
|
|
else:
|
|
|
|
visible = self._text_ctrl.IsShownOnScreen()
|
|
|
|
|
|
focus_remains_on_self_or_children = ClientGUICommon.WindowOrAnyTLPChildHasFocus( self )
|
|
|
|
return tlp_active and visible and focus_remains_on_self_or_children
|
|
|
|
|
|
def _ShouldTakeResponsibilityForEnter( self ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def _ShowDropdown( self ):
|
|
|
|
( text_width, text_height ) = self._text_ctrl.GetSize()
|
|
|
|
if self._text_ctrl.IsShown():
|
|
|
|
desired_dropdown_position = self._text_ctrl.ClientToScreen( ( -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 _UpdateBackgroundColour( self ):
|
|
|
|
colour = HG.client_controller.new_options.GetColour( CC.COLOUR_AUTOCOMPLETE_BACKGROUND )
|
|
|
|
if not self._intercept_key_events:
|
|
|
|
colour = ClientData.GetLighterDarkerColour( colour )
|
|
|
|
|
|
self._text_ctrl.SetBackgroundColour( colour )
|
|
|
|
self._text_ctrl.Refresh()
|
|
|
|
|
|
def _UpdateList( self ):
|
|
|
|
pass
|
|
|
|
|
|
def BroadcastChoices( self, predicates ):
|
|
|
|
self._BroadcastChoices( predicates )
|
|
|
|
|
|
def DropdownHideShow( self ):
|
|
|
|
try:
|
|
|
|
if self._ShouldShow():
|
|
|
|
self._ShowDropdown()
|
|
|
|
if self._move_hide_job is not None:
|
|
|
|
self._move_hide_job.Cancel()
|
|
|
|
self._move_hide_job = None
|
|
|
|
|
|
else:
|
|
|
|
self._HideDropdown()
|
|
|
|
|
|
except:
|
|
|
|
if self._move_hide_job is not None:
|
|
|
|
self._move_hide_job.Cancel()
|
|
|
|
self._move_hide_job = None
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
def EventCharHook( self, event ):
|
|
|
|
HG.client_controller.ResetIdleTimer()
|
|
|
|
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
|
|
|
|
if key in ( wx.WXK_INSERT, wx.WXK_NUMPAD_INSERT ):
|
|
|
|
self._intercept_key_events = not self._intercept_key_events
|
|
|
|
self._UpdateBackgroundColour()
|
|
|
|
elif key == wx.WXK_SPACE and event.RawControlDown(): # this is control, not command on os x, for which command+space does some os stuff
|
|
|
|
self._ScheduleListRefresh( 0.0 )
|
|
|
|
elif self._intercept_key_events:
|
|
|
|
if key in ( ord( 'A' ), ord( 'a' ) ) and modifier == wx.ACCEL_CTRL:
|
|
|
|
event.Skip()
|
|
|
|
elif key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ) and self._ShouldTakeResponsibilityForEnter():
|
|
|
|
self._TakeResponsibilityForEnter()
|
|
|
|
elif key 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 key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ):
|
|
|
|
new_event = SelectUpEvent( -1 )
|
|
|
|
elif key in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ):
|
|
|
|
new_event = SelectDownEvent( -1 )
|
|
|
|
|
|
wx.QueueEvent( self.GetEventHandler(), new_event )
|
|
|
|
elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
|
|
|
|
if key in ( wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ):
|
|
|
|
new_event = ShowPreviousEvent( -1 )
|
|
|
|
elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN ):
|
|
|
|
new_event = ShowNextEvent( -1 )
|
|
|
|
|
|
wx.QueueEvent( self.GetEventHandler(), new_event )
|
|
|
|
else:
|
|
|
|
# Don't say QueueEvent here--it duplicates the event processing at higher levels, leading to 2 x F9, for instance
|
|
self._dropdown_list.EventCharHook( event ) # this typically skips the event, letting the text ctrl take it
|
|
|
|
|
|
else:
|
|
|
|
event.Skip()
|
|
|
|
|
|
|
|
def EventCloseDropdown( self, event ):
|
|
|
|
HG.client_controller.GetGUI().Close()
|
|
|
|
|
|
def EventKillFocus( self, event ):
|
|
|
|
if self._float_mode:
|
|
|
|
self.DropdownHideShow()
|
|
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventMouseWheel( self, event ):
|
|
|
|
if self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
|
|
|
|
if event.GetWheelRotation() > 0:
|
|
|
|
new_event = SelectUpEvent( -1 )
|
|
|
|
else:
|
|
|
|
new_event = SelectDownEvent( -1 )
|
|
|
|
|
|
wx.QueueEvent( self.GetEventHandler(), new_event )
|
|
|
|
else:
|
|
|
|
if event.CmdDown():
|
|
|
|
if event.GetWheelRotation() > 0:
|
|
|
|
self._dropdown_list.MoveSelectionUp()
|
|
|
|
else:
|
|
|
|
self._dropdown_list.MoveSelectionDown()
|
|
|
|
|
|
else:
|
|
|
|
# for some reason, the scrolledwindow list doesn't process scroll events properly when in a popupwindow
|
|
# so let's just tell it to scroll manually
|
|
|
|
( start_x, start_y ) = 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.QueueEvent( self._dropdown_list.GetEventHandler(), wx.ScrollWinEvent( command_type ) )
|
|
|
|
|
|
|
|
|
|
def EventMove( self, event ):
|
|
|
|
self._ParentMovedOrResized()
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventSetFocus( self, event ):
|
|
|
|
if self._float_mode:
|
|
|
|
self.DropdownHideShow()
|
|
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventText( self, event ):
|
|
|
|
num_chars = len( self._text_ctrl.GetValue() )
|
|
|
|
if num_chars == 0:
|
|
|
|
self._ScheduleListRefresh( 0.0 )
|
|
|
|
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._ScheduleListRefresh( 0.0 )
|
|
|
|
elif num_chars < char_limit:
|
|
|
|
self._ScheduleListRefresh( long_wait / 1000.0 )
|
|
|
|
else:
|
|
|
|
self._ScheduleListRefresh( short_wait / 1000.0 )
|
|
|
|
|
|
|
|
|
|
def ForceSizeCalcNow( self ):
|
|
|
|
self.DropdownHideShow()
|
|
|
|
|
|
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_matches = []
|
|
|
|
file_service = HG.client_controller.services_manager.GetService( self._file_service_key )
|
|
|
|
tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )
|
|
|
|
self._file_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, file_service.GetName(), self.FileButtonHit )
|
|
self._file_repo_button.SetMinSize( ( 20, -1 ) )
|
|
|
|
self._tag_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, tag_service.GetName(), self.TagButtonHit )
|
|
self._tag_repo_button.SetMinSize( ( 20, -1 ) )
|
|
|
|
|
|
def _ChangeFileService( self, file_service_key ):
|
|
|
|
if file_service_key == CC.COMBINED_FILE_SERVICE_KEY and self._tag_service_key == CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
self._ChangeTagService( CC.LOCAL_TAG_SERVICE_KEY )
|
|
|
|
|
|
self._file_service_key = file_service_key
|
|
|
|
file_service = HG.client_controller.services_manager.GetService( self._file_service_key )
|
|
|
|
name = file_service.GetName()
|
|
|
|
self._file_repo_button.SetLabelText( name )
|
|
|
|
self._SetListDirty()
|
|
|
|
|
|
def _ChangeTagService( self, tag_service_key ):
|
|
|
|
if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY and self._file_service_key == CC.COMBINED_FILE_SERVICE_KEY:
|
|
|
|
self._ChangeFileService( CC.LOCAL_FILE_SERVICE_KEY )
|
|
|
|
|
|
self._tag_service_key = tag_service_key
|
|
|
|
self._dropdown_list.SetTagService( self._tag_service_key )
|
|
|
|
tag_service = tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )
|
|
|
|
name = tag_service.GetName()
|
|
|
|
self._tag_repo_button.SetLabelText( name )
|
|
|
|
self._cache_text = None
|
|
|
|
self._SetListDirty()
|
|
|
|
|
|
def _UpdateList( self ):
|
|
|
|
self._refresh_list_job = None
|
|
|
|
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:
|
|
|
|
# refresh system preds after five mins
|
|
|
|
self._ScheduleListRefresh( 300 )
|
|
|
|
|
|
|
|
def FileButtonHit( self ):
|
|
|
|
services_manager = HG.client_controller.services_manager
|
|
|
|
services = []
|
|
|
|
services.append( services_manager.GetService( CC.LOCAL_FILE_SERVICE_KEY ) )
|
|
services.append( services_manager.GetService( CC.TRASH_SERVICE_KEY ) )
|
|
services.append( services_manager.GetService( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
|
|
services.extend( services_manager.GetServices( ( HC.FILE_REPOSITORY, ) ) )
|
|
|
|
advanced_mode = HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
|
|
|
|
if advanced_mode:
|
|
|
|
services.append( services_manager.GetService( CC.COMBINED_FILE_SERVICE_KEY ) )
|
|
|
|
|
|
menu = wx.Menu()
|
|
|
|
for service in services:
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, service.GetName(), 'Change the current file domain to ' + service.GetName() + '.', self._ChangeFileService, service.GetServiceKey() )
|
|
|
|
|
|
HG.client_controller.PopupMenu( self._file_repo_button, menu )
|
|
|
|
|
|
def SetFileService( self, file_service_key ):
|
|
|
|
self._ChangeFileService( file_service_key )
|
|
|
|
|
|
def SetTagService( self, tag_service_key ):
|
|
|
|
self._ChangeTagService( tag_service_key )
|
|
|
|
|
|
def TagButtonHit( self ):
|
|
|
|
services_manager = HG.client_controller.services_manager
|
|
|
|
services = []
|
|
|
|
services.append( services_manager.GetService( CC.LOCAL_TAG_SERVICE_KEY ) )
|
|
services.extend( services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) )
|
|
services.append( services_manager.GetService( CC.COMBINED_TAG_SERVICE_KEY ) )
|
|
|
|
menu = wx.Menu()
|
|
|
|
for service in services:
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, service.GetName(), 'Change the current tag domain to ' + service.GetName() + '.', self._ChangeTagService, service.GetServiceKey() )
|
|
|
|
|
|
HG.client_controller.PopupMenu( self._tag_repo_button, menu )
|
|
|
|
|
|
class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
|
|
|
|
def __init__( self, parent, page_key, file_search_context, media_callable = None, synchronised = True, include_unusual_predicate_types = True ):
|
|
|
|
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.SetToolTip( '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.SetToolTip( '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.SetToolTip( '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.Add( self._include_current_tags, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
button_hbox_1.Add( self._include_pending_tags, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
button_hbox_2 = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
button_hbox_2.Add( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
button_hbox_2.Add( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.Add( button_hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.Add( self._synchronised, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.Add( button_hbox_2, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.Add( self._dropdown_list, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._dropdown_window.SetSizer( vbox )
|
|
|
|
HG.client_controller.sub( self, 'SetSynchronisedWait', 'synchronised_wait_switch' )
|
|
|
|
HG.client_controller.sub( self, 'IncludeCurrent', 'notify_include_current' )
|
|
HG.client_controller.sub( self, 'IncludePending', 'notify_include_pending' )
|
|
|
|
|
|
def _BroadcastChoices( self, predicates ):
|
|
|
|
if self._text_ctrl.GetValue() != '':
|
|
|
|
self._text_ctrl.SetValue( '' )
|
|
|
|
|
|
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates )
|
|
|
|
|
|
def _BroadcastCurrentText( self ):
|
|
|
|
( inclusive, search_text, explicit_wildcard, cache_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 )
|
|
|
|
HG.client_controller.pub( 'change_file_service', self._page_key, file_service_key )
|
|
|
|
HG.client_controller.pub( 'refresh_query', self._page_key )
|
|
|
|
|
|
def _ChangeTagService( self, tag_service_key ):
|
|
|
|
AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )
|
|
|
|
self._file_search_context.SetTagServiceKey( tag_service_key )
|
|
|
|
HG.client_controller.pub( 'change_tag_service', self._page_key, tag_service_key )
|
|
|
|
HG.client_controller.pub( 'refresh_query', self._page_key )
|
|
|
|
|
|
def _InitDropDownList( self ):
|
|
|
|
return ClientGUIListBoxes.ListBoxTagsACRead( self._dropdown_window, self.BroadcastChoices, self._tag_service_key, min_height = self._list_height )
|
|
|
|
|
|
def _ParseSearchText( self ):
|
|
|
|
raw_entry = self._text_ctrl.GetValue()
|
|
|
|
if raw_entry.startswith( '-' ):
|
|
|
|
inclusive = False
|
|
|
|
entry_text = raw_entry[1:]
|
|
|
|
else:
|
|
|
|
inclusive = True
|
|
|
|
entry_text = raw_entry
|
|
|
|
|
|
tag = HydrusTags.CleanTag( entry_text )
|
|
|
|
explicit_wildcard = '*' in entry_text
|
|
|
|
search_text = ClientSearch.ConvertEntryTextToSearchText( entry_text )
|
|
|
|
if explicit_wildcard:
|
|
|
|
cache_text = None
|
|
|
|
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive )
|
|
|
|
else:
|
|
|
|
cache_text = search_text[:-1] # take off the trailing '*' for the cache text
|
|
|
|
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
|
|
|
sibling = siblings_manager.GetSibling( self._tag_service_key, tag )
|
|
|
|
if sibling is None:
|
|
|
|
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag, inclusive )
|
|
|
|
else:
|
|
|
|
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, sibling, inclusive )
|
|
|
|
|
|
|
|
return ( inclusive, search_text, explicit_wildcard, cache_text, entry_predicate )
|
|
|
|
|
|
def _GenerateMatches( self ):
|
|
|
|
self._next_updatelist_is_probably_fast = False
|
|
|
|
num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]
|
|
|
|
( inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = self._ParseSearchText()
|
|
|
|
if search_text in ( '', ':', '*' ):
|
|
|
|
input_just_changed = self._cache_text is not None
|
|
|
|
db_not_going_to_hang_if_we_hit_it = not HG.client_controller.DBCurrentlyDoingJob()
|
|
|
|
if input_just_changed or db_not_going_to_hang_if_we_hit_it or not self._initial_matches_fetched:
|
|
|
|
self._cache_text = None
|
|
|
|
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 = HG.client_controller.Read( 'file_system_predicates', search_service_key )
|
|
|
|
|
|
matches = self._cached_results
|
|
|
|
else:
|
|
|
|
( namespace, half_complete_subtag ) = HydrusTags.SplitTag( search_text )
|
|
|
|
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
|
|
|
if False and half_complete_subtag == '':
|
|
|
|
self._cache_text = None
|
|
|
|
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:
|
|
|
|
# if user searches 'blah', then we include 'blah (23)' for 'series:blah (10)', 'blah (13)'
|
|
# if they search for 'series:blah', then we don't!
|
|
add_namespaceless = ':' not in namespace
|
|
|
|
include_current = self._file_search_context.IncludeCurrentTags()
|
|
include_pending = self._file_search_context.IncludePendingTags()
|
|
|
|
small_and_specific_search = cache_text is not None and len( cache_text ) < num_autocomplete_chars
|
|
|
|
if small_and_specific_search:
|
|
|
|
predicates = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = cache_text, exact_match = True, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = add_namespaceless, collapse_siblings = True )
|
|
|
|
else:
|
|
|
|
cache_invalid_for_this_search = cache_text is None or self._cache_text is None or not cache_text.startswith( self._cache_text )
|
|
|
|
if cache_invalid_for_this_search:
|
|
|
|
self._cache_text = cache_text
|
|
|
|
self._cached_results = HG.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 = add_namespaceless, 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.FilterTagsBySearchText( 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.FilterTagsBySearchText( 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 )
|
|
|
|
|
|
if namespace == '':
|
|
|
|
predicates = ClientData.MergePredicates( predicates, add_namespaceless = True )
|
|
|
|
|
|
self._next_updatelist_is_probably_fast = True
|
|
|
|
|
|
matches = ClientSearch.FilterPredicatesBySearchText( self._tag_service_key, search_text, predicates )
|
|
|
|
matches = ClientSearch.SortPredicates( matches )
|
|
|
|
|
|
if self._include_unusual_predicate_types:
|
|
|
|
if explicit_wildcard:
|
|
|
|
matches.insert( 0, ClientSearch.Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive ) )
|
|
|
|
else:
|
|
|
|
if namespace != '' and half_complete_subtag in ( '', '*' ):
|
|
|
|
matches.insert( 0, ClientSearch.Predicate( HC.PREDICATE_TYPE_NAMESPACE, namespace, inclusive ) )
|
|
|
|
|
|
|
|
|
|
for match in matches:
|
|
|
|
if match.GetInclusive() != inclusive: match.SetInclusive( inclusive )
|
|
|
|
|
|
try:
|
|
|
|
index = matches.index( entry_predicate )
|
|
|
|
predicate = matches[ index ]
|
|
|
|
del matches[ index ]
|
|
|
|
matches.insert( 0, predicate )
|
|
|
|
except:
|
|
|
|
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 )
|
|
|
|
self._SetListDirty()
|
|
|
|
HG.client_controller.pub( 'refresh_query', self._page_key )
|
|
|
|
|
|
|
|
def IncludePending( self, page_key, value ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
self._file_search_context.SetIncludePendingTags( value )
|
|
|
|
self._SetListDirty()
|
|
|
|
HG.client_controller.pub( 'refresh_query', self._page_key )
|
|
|
|
|
|
|
|
def 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
|
|
|
|
|
|
if tag_service_key == CC.LOCAL_TAG_SERVICE_KEY:
|
|
|
|
file_service_key = CC.LOCAL_FILE_SERVICE_KEY
|
|
|
|
|
|
AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox.Add( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
hbox.Add( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.Add( self._dropdown_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()
|
|
|
|
tag = HydrusTags.CleanTag( raw_entry )
|
|
|
|
search_text = ClientSearch.ConvertEntryTextToSearchText( raw_entry )
|
|
|
|
if ClientSearch.IsComplexWildcard( search_text ):
|
|
|
|
cache_text = None
|
|
|
|
else:
|
|
|
|
cache_text = search_text[:-1] # take off the trailing '*' for the cache text
|
|
|
|
|
|
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag )
|
|
|
|
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
|
|
|
sibling = siblings_manager.GetSibling( self._tag_service_key, tag )
|
|
|
|
if sibling is not None:
|
|
|
|
sibling_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, sibling )
|
|
|
|
else:
|
|
|
|
sibling_predicate = None
|
|
|
|
|
|
return ( search_text, cache_text, entry_predicate, sibling_predicate )
|
|
|
|
|
|
def _BroadcastCurrentText( self ):
|
|
|
|
( search_text, cache_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, cache_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()
|
|
|
|
if search_text in ( '', ':', '*' ):
|
|
|
|
self._cache_text = None
|
|
|
|
matches = []
|
|
|
|
else:
|
|
|
|
must_do_a_search = False
|
|
|
|
small_and_specific_search = cache_text is not None and len( cache_text ) < num_autocomplete_chars
|
|
|
|
if small_and_specific_search:
|
|
|
|
predicates = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, search_text = cache_text, exact_match = True, add_namespaceless = False, collapse_siblings = False )
|
|
|
|
else:
|
|
|
|
cache_invalid_for_this_search = cache_text is None or self._cache_text is None or not cache_text.startswith( self._cache_text )
|
|
|
|
if must_do_a_search or cache_invalid_for_this_search:
|
|
|
|
self._cache_text = cache_text
|
|
|
|
self._cached_results = HG.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.FilterPredicatesBySearchText( self._tag_service_key, search_text, 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 = HG.client_controller.GetManager( 'tag_parents' )
|
|
|
|
matches = parents_manager.ExpandPredicates( self._tag_service_key, matches )
|
|
|
|
|
|
|
|
return matches
|
|
|
|
|
|
def _InitDropDownList( self ):
|
|
|
|
return ClientGUIListBoxes.ListBoxTagsACWrite( self._dropdown_window, self.BroadcastChoices, self._tag_service_key, 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()
|
|
|
|
|
|
|