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