hydrus/include/ClientGUIACDropdown.py

1890 lines
64 KiB
Python
Raw Normal View History

2019-01-09 22:59:03 +00:00
from . import ClientCaches
from . import ClientConstants as CC
from . import ClientData
from . import ClientGUICommon
2019-06-26 21:27:18 +00:00
from . import ClientGUIFunctions
2019-01-09 22:59:03 +00:00
from . import ClientGUIListBoxes
from . import ClientGUIMenus
from . import ClientGUIShortcuts
from . import ClientSearch
2019-01-30 22:14:54 +00:00
from . import ClientThreading
2016-09-21 19:54:04 +00:00
import collections
2019-01-09 22:59:03 +00:00
from . import HydrusConstants as HC
from . import HydrusData
from . import HydrusExceptions
from . import HydrusGlobals as HG
from . import HydrusTags
2019-06-26 21:27:18 +00:00
from . import HydrusText
2016-09-21 19:54:04 +00:00
import itertools
import wx
2019-04-03 22:45:57 +00:00
import wx.lib.scrolledpanel
2016-09-21 19:54:04 +00:00
ID_TIMER_DROPDOWN_HIDE = wx.NewId()
ID_TIMER_AC_LAG = wx.NewId()
2018-01-31 22:58:15 +00:00
( 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()
2019-04-10 22:50:53 +00:00
def ReadFetch( win, job_key, results_callable, parsed_search_text, wx_media_callable, file_search_context, synchronised, include_unusual_predicate_types, initial_matches_fetched, search_text_for_current_cache, cached_results, under_construction_or_predicate ):
2019-01-30 22:14:54 +00:00
next_search_is_probably_fast = False
include_current = file_search_context.IncludeCurrentTags()
include_pending = file_search_context.IncludePendingTags()
file_service_key = file_search_context.GetFileServiceKey()
tag_service_key = file_search_context.GetTagServiceKey()
num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]
( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = parsed_search_text
if search_text in ( '', ':', '*' ):
# if the user inputs '-' or similar, let's go to an empty list
if raw_entry == '':
input_just_changed = search_text_for_current_cache 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 initial_matches_fetched:
if file_service_key == CC.COMBINED_FILE_SERVICE_KEY:
search_service_key = tag_service_key
else:
search_service_key = file_service_key
search_text_for_current_cache = None
cached_results = HG.client_controller.Read( 'file_system_predicates', search_service_key )
matches = cached_results
else:
matches = []
else:
( namespace, half_complete_subtag ) = HydrusTags.SplitTag( search_text )
2019-04-17 21:51:50 +00:00
siblings_manager = HG.client_controller.tag_siblings_manager
2019-01-30 22:14:54 +00:00
if False and half_complete_subtag == '':
search_text_for_current_cache = None
matches = [] # a query like 'namespace:'
else:
fetch_from_db = True
if synchronised and wx_media_callable is not None:
try:
media = HG.client_controller.CallBlockingToWX( win, wx_media_callable )
except HydrusExceptions.WXDeadWindowException:
return
can_fetch_from_media = media is not None and len( media ) > 0
if can_fetch_from_media:
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
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 = file_service_key, tag_service_key = tag_service_key, search_text = cache_text, exact_match = True, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = add_namespaceless, job_key = job_key, collapse_siblings = True )
else:
cache_invalid_for_this_search = cache_text is None or search_text_for_current_cache is None or not cache_text.startswith( search_text_for_current_cache )
if cache_invalid_for_this_search:
new_search_text_for_current_cache = cache_text
cached_results = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = search_text, inclusive = inclusive, include_current = include_current, include_pending = include_pending, add_namespaceless = add_namespaceless, job_key = job_key, collapse_siblings = True )
predicates = cached_results
next_search_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 include_current:
lists_of_current_tags = [ list( tags_manager.GetCurrent( 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( tag_service_key, search_text, current_tags_flat_iterable )
current_tags_to_count.update( current_tags_flat )
tags_to_do.update( list(current_tags_to_count.keys()) )
if include_pending:
lists_of_pending_tags = [ list( tags_manager.GetPending( 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( tag_service_key, search_text, pending_tags_flat_iterable )
pending_tags_to_count.update( pending_tags_flat )
tags_to_do.update( list(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 tag_service_key != CC.COMBINED_TAG_SERVICE_KEY:
predicates = siblings_manager.CollapsePredicates( tag_service_key, predicates )
if namespace == '':
predicates = ClientData.MergePredicates( predicates, add_namespaceless = True )
next_search_is_probably_fast = True
matches = ClientSearch.FilterPredicatesBySearchText( tag_service_key, search_text, predicates )
matches = ClientSearch.SortPredicates( matches )
if 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
2019-04-10 22:50:53 +00:00
if under_construction_or_predicate is not None:
matches.insert( 0, under_construction_or_predicate )
2019-01-30 22:14:54 +00:00
if job_key.IsCancelled():
return
HG.client_controller.CallLaterWXSafe( win, 0.0, results_callable, job_key, search_text, search_text_for_current_cache, cached_results, matches, next_search_is_probably_fast )
def PutAtTopOfMatches( matches, predicate ):
try:
index = matches.index( predicate )
predicate = matches[ index ]
matches.remove( predicate )
except ValueError:
pass
matches.insert( 0, predicate )
def WriteFetch( win, job_key, results_callable, parsed_search_text, file_service_key, tag_service_key, expand_parents, search_text_for_current_cache, cached_results ):
next_search_is_probably_fast = False
num_autocomplete_chars = HC.options[ 'num_autocomplete_chars' ]
( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = parsed_search_text
if search_text in ( '', ':', '*' ):
search_text_for_current_cache = 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 = file_service_key, tag_service_key = tag_service_key, search_text = cache_text, exact_match = True, add_namespaceless = False, job_key = job_key, collapse_siblings = False )
else:
cache_invalid_for_this_search = cache_text is None or search_text_for_current_cache is None or not cache_text.startswith( search_text_for_current_cache )
if must_do_a_search or cache_invalid_for_this_search:
search_text_for_current_cache = cache_text
cached_results = HG.client_controller.Read( 'autocomplete_predicates', file_service_key = file_service_key, tag_service_key = tag_service_key, search_text = search_text, add_namespaceless = False, job_key = job_key, collapse_siblings = False )
predicates = cached_results
next_search_is_probably_fast = True
matches = ClientSearch.FilterPredicatesBySearchText( tag_service_key, search_text, predicates )
matches = ClientSearch.SortPredicates( matches )
PutAtTopOfMatches( matches, entry_predicate )
if sibling_predicate is not None:
PutAtTopOfMatches( matches, sibling_predicate )
if expand_parents:
2019-04-17 21:51:50 +00:00
parents_manager = HG.client_controller.tag_parents_manager
2019-01-30 22:14:54 +00:00
matches = parents_manager.ExpandPredicates( tag_service_key, matches )
HG.client_controller.CallLaterWXSafe( win, 0.0, results_callable, job_key, search_text, search_text_for_current_cache, cached_results, matches, next_search_is_probably_fast )
2016-09-21 19:54:04 +00:00
# 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
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"
2018-02-21 21:59:37 +00:00
not_main_gui = tlp.GetParent() is not None
2019-02-27 23:03:30 +00:00
if not_main_gui or HC.options[ 'always_embed_autocompletes' ] or not HC.PLATFORM_WINDOWS:
2016-09-21 19:54:04 +00:00
self._float_mode = False
else:
self._float_mode = True
2019-05-15 20:35:00 +00:00
self._text_ctrl = wx.TextCtrl( self, style = wx.TE_PROCESS_ENTER )
2016-09-21 19:54:04 +00:00
2017-09-20 19:47:31 +00:00
self._UpdateBackgroundColour()
2016-09-21 19:54:04 +00:00
self._last_attempted_dropdown_width = 0
self._last_attempted_dropdown_position = ( None, None )
2018-02-21 21:59:37 +00:00
self._last_move_event_started = 0.0
self._last_move_event_occurred = 0.0
2016-09-21 19:54:04 +00:00
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 )
2017-04-19 20:58:30 +00:00
self._text_ctrl.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
2016-09-21 19:54:04 +00:00
self._text_ctrl.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
vbox = wx.BoxSizer( wx.VERTICAL )
2019-06-26 21:27:18 +00:00
self._text_input_hbox = wx.BoxSizer( wx.HORIZONTAL )
self._text_input_hbox.Add( self._text_ctrl, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
vbox.Add( self._text_input_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
2016-09-21 19:54:04 +00:00
#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 ) )
2019-06-26 21:27:18 +00:00
self._dropdown_window.SetPosition( ClientGUIFunctions.ClientToScreen( self._text_ctrl, ( 0, 0 ) ) )
2016-09-21 19:54:04 +00:00
self._dropdown_window.Bind( wx.EVT_CLOSE, self.EventCloseDropdown )
self._dropdown_hidden = True
2018-05-23 21:05:06 +00:00
self._list_height_num_chars = 19
2016-09-21 19:54:04 +00:00
else:
self._dropdown_window = wx.Panel( self )
2018-10-31 21:41:14 +00:00
self._list_height_num_chars = 8
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
self._dropdown_notebook = wx.Notebook( self._dropdown_window )
#
self._search_results_list = self._InitSearchResultsList()
self._dropdown_notebook.AddPage( self._search_results_list, 'results', True )
#
2016-09-21 19:54:04 +00:00
2018-02-07 23:40:33 +00:00
if not self._float_mode:
vbox.Add( self._dropdown_window, CC.FLAGS_EXPAND_BOTH_WAYS )
2016-09-21 19:54:04 +00:00
self.SetSizer( vbox )
2019-01-30 22:14:54 +00:00
self._last_fetched_search_text = ''
self._next_search_is_probably_fast = False
self._search_text_for_current_cache = None
2016-09-21 19:54:04 +00:00
self._cached_results = []
2019-01-30 22:14:54 +00:00
self._current_fetch_job_key = None
2016-09-21 19:54:04 +00:00
self._initial_matches_fetched = False
2018-02-14 21:47:18 +00:00
self._move_hide_job = None
2019-01-30 22:14:54 +00:00
self._refresh_list_job = None
2018-01-03 22:37:30 +00:00
2016-09-21 19:54:04 +00:00
if self._float_mode:
self.Bind( wx.EVT_MOVE, self.EventMove )
self.Bind( wx.EVT_SIZE, self.EventMove )
2018-02-21 21:59:37 +00:00
HG.client_controller.sub( self, '_ParentMovedOrResized', 'main_gui_move_event' )
2016-09-21 19:54:04 +00:00
parent = self
while True:
try:
parent = parent.GetParent()
if isinstance( parent, wx.ScrolledWindow ):
parent.Bind( wx.EVT_SCROLLWIN, self.EventMove )
except:
break
2017-09-20 19:47:31 +00:00
HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
2018-06-27 19:27:05 +00:00
HG.client_controller.sub( self, 'DoDropdownHideShow', 'notify_page_change' )
2017-09-20 19:47:31 +00:00
2018-02-14 21:47:18 +00:00
self._ScheduleListRefresh( 0.0 )
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
def _BroadcastChoices( self, predicates, shift_down ):
2016-09-21 19:54:04 +00:00
raise NotImplementedError()
2019-04-03 22:45:57 +00:00
def _BroadcastCurrentText( self, shift_down ):
2016-09-21 19:54:04 +00:00
text = self._text_ctrl.GetValue()
2019-04-03 22:45:57 +00:00
self._BroadcastChoices( { text }, shift_down )
2016-09-21 19:54:04 +00:00
2019-06-19 22:08:48 +00:00
def _CancelCurrentResultsFetchJob( self ):
if self._current_fetch_job_key is not None:
self._current_fetch_job_key.Cancel()
2018-02-14 21:47:18 +00:00
def _CancelScheduledListRefresh( self ):
if self._refresh_list_job is not None:
self._refresh_list_job.Cancel()
2018-05-23 21:05:06 +00:00
def _ClearInput( self ):
self._text_ctrl.SetValue( '' )
2019-04-10 22:50:53 +00:00
self._SetResultsToList( [] )
2018-05-23 21:05:06 +00:00
self._ScheduleListRefresh( 0.0 )
2018-02-28 22:30:36 +00:00
def _DropdownHideShow( self ):
if not self._float_mode:
return
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
2019-04-10 22:50:53 +00:00
def _HandleEscape( self ):
if self._float_mode:
self.GetTopLevelParent().SetFocus()
return True
else:
return False
2016-09-21 19:54:04 +00:00
def _HideDropdown( self ):
if not self._dropdown_hidden:
2018-03-28 21:55:58 +00:00
self._dropdown_window.Hide()
2016-09-21 19:54:04 +00:00
self._dropdown_hidden = True
2018-03-28 21:55:58 +00:00
def _InitSearchResultsList( self ):
raise NotImplementedError()
2018-02-21 21:59:37 +00:00
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:
2018-05-16 20:09:50 +00:00
self._move_hide_job = HG.client_controller.CallRepeatingWXSafe( self._dropdown_window, 0.0, 0.25, self._DropdownHideShow )
2018-02-21 21:59:37 +00:00
self._move_hide_job.Delay( 0.25 )
else:
2018-02-28 22:30:36 +00:00
self._DropdownHideShow()
2018-02-21 21:59:37 +00:00
2018-02-14 21:47:18 +00:00
def _ScheduleListRefresh( self, delay ):
if self._refresh_list_job is not None and delay == 0.0:
2018-05-16 20:09:50 +00:00
self._refresh_list_job.Wake()
2018-02-14 21:47:18 +00:00
else:
self._CancelScheduledListRefresh()
2018-03-28 21:55:58 +00:00
self._refresh_list_job = HG.client_controller.CallLaterWXSafe( self, delay, self._UpdateSearchResultsList )
2018-02-14 21:47:18 +00:00
def _SetListDirty( self ):
2019-01-30 22:14:54 +00:00
self._search_text_for_current_cache = None
2018-02-14 21:47:18 +00:00
self._ScheduleListRefresh( 0.0 )
2019-01-30 22:14:54 +00:00
def _SetResultsToList( self, results ):
raise NotImplementedError()
2016-09-21 19:54:04 +00:00
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!
2017-05-10 21:33:58 +00:00
gui = HG.client_controller.GetGUI()
2016-09-21 19:54:04 +00:00
current_page = gui.GetCurrentPage()
2019-06-26 21:27:18 +00:00
visible = ClientGUIFunctions.IsWXAncestor( self, current_page )
2016-09-21 19:54:04 +00:00
else:
visible = self._text_ctrl.IsShownOnScreen()
2019-06-26 21:27:18 +00:00
focus_remains_on_self_or_children = ClientGUIFunctions.WindowOrAnyTLPChildHasFocus( self )
2016-09-21 19:54:04 +00:00
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()
2017-05-31 21:50:53 +00:00
if self._text_ctrl.IsShown():
2016-09-21 19:54:04 +00:00
2019-06-26 21:27:18 +00:00
desired_dropdown_position = ClientGUIFunctions.ClientToScreen( self._text_ctrl, ( -2, text_height - 2 ) )
2016-09-21 19:54:04 +00:00
2017-05-31 21:50:53 +00:00
if self._last_attempted_dropdown_position != desired_dropdown_position:
self._dropdown_window.SetPosition( desired_dropdown_position )
self._last_attempted_dropdown_position = desired_dropdown_position
2016-09-21 19:54:04 +00:00
#
show_and_fit_needed = False
if self._dropdown_hidden:
2018-03-28 21:55:58 +00:00
self._dropdown_window.Show()
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
self._dropdown_hidden = False
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if text_width != self._last_attempted_dropdown_width:
show_and_fit_needed = True
2016-09-21 19:54:04 +00:00
self._dropdown_window.Fit()
self._dropdown_window.SetSize( ( text_width, -1 ) )
self._dropdown_window.Layout()
self._last_attempted_dropdown_width = text_width
2019-01-30 22:14:54 +00:00
def _StartResultsFetchJob( self, job_key ):
raise NotImplementedError()
2019-04-03 22:45:57 +00:00
def _TakeResponsibilityForEnter( self, shift_down ):
2016-09-21 19:54:04 +00:00
raise NotImplementedError()
2017-09-20 19:47:31 +00:00
def _UpdateBackgroundColour( self ):
2017-12-06 22:06:56 +00:00
colour = HG.client_controller.new_options.GetColour( CC.COLOUR_AUTOCOMPLETE_BACKGROUND )
2017-09-20 19:47:31 +00:00
if not self._intercept_key_events:
colour = ClientData.GetLighterDarkerColour( colour )
self._text_ctrl.SetBackgroundColour( colour )
self._text_ctrl.Refresh()
2018-03-28 21:55:58 +00:00
def _UpdateSearchResultsList( self ):
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
self._refresh_list_job = None
2019-06-19 22:08:48 +00:00
self._CancelCurrentResultsFetchJob()
2019-01-30 22:14:54 +00:00
self._current_fetch_job_key = ClientThreading.JobKey( cancellable = True )
self._StartResultsFetchJob( self._current_fetch_job_key )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
def BroadcastChoices( self, predicates, shift_down = False ):
2019-04-03 22:45:57 +00:00
self._BroadcastChoices( predicates, shift_down )
2016-09-21 19:54:04 +00:00
2019-06-19 22:08:48 +00:00
def CancelCurrentResultsFetchJob( self ):
self._CancelCurrentResultsFetchJob()
2018-06-27 19:27:05 +00:00
def DoDropdownHideShow( self ):
self._DropdownHideShow()
2017-04-19 20:58:30 +00:00
def EventCharHook( self, event ):
2016-09-21 19:54:04 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.ResetIdleTimer()
2016-09-21 19:54:04 +00:00
2018-05-16 20:09:50 +00:00
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
2017-04-05 21:16:40 +00:00
if key in ( wx.WXK_INSERT, wx.WXK_NUMPAD_INSERT ):
2016-09-21 19:54:04 +00:00
2017-09-20 19:47:31 +00:00
self._intercept_key_events = not self._intercept_key_events
2017-09-13 20:50:41 +00:00
2017-09-20 19:47:31 +00:00
self._UpdateBackgroundColour()
2016-09-21 19:54:04 +00:00
2017-04-05 21:16:40 +00:00
elif key == wx.WXK_SPACE and event.RawControlDown(): # this is control, not command on os x, for which command+space does some os stuff
2016-09-21 19:54:04 +00:00
2018-02-14 21:47:18 +00:00
self._ScheduleListRefresh( 0.0 )
2016-09-21 19:54:04 +00:00
elif self._intercept_key_events:
2018-03-28 21:55:58 +00:00
send_input_to_current_list = False
current_results_list = self._dropdown_notebook.GetCurrentPage()
current_list_is_empty = len( current_results_list ) == 0
input_is_empty = self._text_ctrl.GetValue() == ''
2017-04-05 21:16:40 +00:00
if key in ( ord( 'A' ), ord( 'a' ) ) and modifier == wx.ACCEL_CTRL:
2016-09-21 19:54:04 +00:00
event.Skip()
2017-04-05 21:16:40 +00:00
elif key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ) and self._ShouldTakeResponsibilityForEnter():
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
shift_down = modifier == wx.ACCEL_SHIFT
self._TakeResponsibilityForEnter( shift_down )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
elif input_is_empty: # maybe we should be sending a 'move' event to a different place
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ) and current_list_is_empty:
2017-09-20 19:47:31 +00:00
2018-03-28 21:55:58 +00:00
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 )
2017-09-20 19:47:31 +00:00
2018-03-28 21:55:58 +00:00
wx.QueueEvent( self.GetEventHandler(), new_event )
2017-09-20 19:47:31 +00:00
2018-03-28 21:55:58 +00:00
elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and current_list_is_empty:
2017-09-20 19:47:31 +00:00
2018-03-28 21:55:58 +00:00
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 )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
wx.QueueEvent( self.GetEventHandler(), new_event )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
elif key in ( wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT, wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT ):
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if key in ( wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT ):
direction = -1
elif key in ( wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT ):
direction = 1
self.MoveNotebookPageFocus( direction = direction )
2019-04-10 22:50:53 +00:00
elif key == wx.WXK_ESCAPE:
escape_caught = self._HandleEscape()
if not escape_caught:
send_input_to_current_list = True
2018-03-28 21:55:58 +00:00
else:
send_input_to_current_list = True
2016-09-21 19:54:04 +00:00
else:
2018-03-28 21:55:58 +00:00
send_input_to_current_list = True
if send_input_to_current_list:
2018-02-21 21:59:37 +00:00
# Don't say QueueEvent here--it duplicates the event processing at higher levels, leading to 2 x F9, for instance
2018-03-28 21:55:58 +00:00
current_results_list.EventCharHook( event ) # ultimately, this typically skips the event, letting the text ctrl take it
2016-09-21 19:54:04 +00:00
else:
event.Skip()
2017-04-19 20:58:30 +00:00
def EventCloseDropdown( self, event ):
2017-05-10 21:33:58 +00:00
HG.client_controller.GetGUI().Close()
2017-04-19 20:58:30 +00:00
2016-09-21 19:54:04 +00:00
def EventKillFocus( self, event ):
2018-02-14 21:47:18 +00:00
if self._float_mode:
2018-01-03 22:37:30 +00:00
2018-02-28 22:30:36 +00:00
self._DropdownHideShow()
2018-01-03 22:37:30 +00:00
2016-09-21 19:54:04 +00:00
event.Skip()
def EventMouseWheel( self, event ):
2018-03-28 21:55:58 +00:00
current_results_list = self._dropdown_notebook.GetCurrentPage()
if self._text_ctrl.GetValue() == '' and len( current_results_list ) == 0:
2016-09-21 19:54:04 +00:00
2018-01-31 22:58:15 +00:00
if event.GetWheelRotation() > 0:
new_event = SelectUpEvent( -1 )
else:
new_event = SelectDownEvent( -1 )
2016-09-21 19:54:04 +00:00
2018-02-21 21:59:37 +00:00
wx.QueueEvent( self.GetEventHandler(), new_event )
2016-09-21 19:54:04 +00:00
else:
if event.CmdDown():
2017-04-19 20:58:30 +00:00
if event.GetWheelRotation() > 0:
2018-03-28 21:55:58 +00:00
current_results_list.MoveSelectionUp()
2017-04-19 20:58:30 +00:00
else:
2018-03-28 21:55:58 +00:00
current_results_list.MoveSelectionDown()
2017-04-19 20:58:30 +00:00
2016-09-21 19:54:04 +00:00
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
2018-03-28 21:55:58 +00:00
( start_x, start_y ) = current_results_list.GetViewStart()
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if event.GetWheelRotation() > 0:
current_results_list.Scroll( -1, start_y - 3 )
else:
current_results_list.Scroll( -1, start_y + 3 )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if event.GetWheelRotation() > 0:
command_type = wx.wxEVT_SCROLLWIN_LINEUP
else:
command_type = wx.wxEVT_SCROLLWIN_LINEDOWN
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
wx.QueueEvent( current_results_list.GetEventHandler(), wx.ScrollWinEvent( command_type ) )
2016-09-21 19:54:04 +00:00
def EventMove( self, event ):
2018-02-21 21:59:37 +00:00
self._ParentMovedOrResized()
2016-09-21 19:54:04 +00:00
event.Skip()
def EventSetFocus( self, event ):
2018-02-14 21:47:18 +00:00
if self._float_mode:
2018-01-03 22:37:30 +00:00
2018-02-28 22:30:36 +00:00
self._DropdownHideShow()
2018-01-03 22:37:30 +00:00
2016-09-21 19:54:04 +00:00
event.Skip()
def EventText( self, event ):
num_chars = len( self._text_ctrl.GetValue() )
if num_chars == 0:
2018-02-14 21:47:18 +00:00
self._ScheduleListRefresh( 0.0 )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
else:
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if HC.options[ 'fetch_ac_results_automatically' ]:
2018-02-07 23:40:33 +00:00
2018-03-28 21:55:58 +00:00
( char_limit, long_wait, short_wait ) = HC.options[ 'ac_timings' ]
2018-02-07 23:40:33 +00:00
2019-01-30 22:14:54 +00:00
self._next_search_is_probably_fast = self._next_search_is_probably_fast and num_chars > len( self._last_fetched_search_text )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if self._next_search_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 )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if self._dropdown_notebook.GetCurrentPage() != self._search_results_list:
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
self.MoveNotebookPageFocus( index = 0 )
2018-01-03 22:37:30 +00:00
2016-09-21 19:54:04 +00:00
2018-02-21 21:59:37 +00:00
def ForceSizeCalcNow( self ):
2018-02-28 22:30:36 +00:00
if self._float_mode:
self._DropdownHideShow()
2018-02-21 21:59:37 +00:00
2018-03-28 21:55:58 +00:00
def MoveNotebookPageFocus( self, index = None, direction = None ):
new_index = None
if index is not None:
new_index = index
elif direction is not None:
current_index = self._dropdown_notebook.GetSelection()
if current_index is not None and current_index != wx.NOT_FOUND:
number_of_pages = self._dropdown_notebook.GetPageCount()
new_index = ( current_index + direction ) % number_of_pages # does wraparound
if new_index is not None:
self._dropdown_notebook.ChangeSelection( new_index )
self._text_ctrl.SetFocus()
2019-01-30 22:14:54 +00:00
def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results ):
2018-03-28 21:55:58 +00:00
2019-01-30 22:14:54 +00:00
if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
self._last_fetched_search_text = search_text
self._search_text_for_current_cache = search_text_for_cache
self._cached_results = cached_results
self._current_fetch_job_key = None
self._initial_matches_fetched = True
self._SetResultsToList( results )
2018-03-28 21:55:58 +00:00
2018-05-16 20:09:50 +00:00
def SetFocus( self ):
if HC.PLATFORM_OSX:
wx.CallAfter( self._text_ctrl.SetFocus )
else:
self._text_ctrl.SetFocus()
2016-09-21 19:54:04 +00:00
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 )
2019-04-24 22:18:50 +00:00
self._allow_all_known_files = True
2017-06-28 20:23:21 +00:00
file_service = HG.client_controller.services_manager.GetService( self._file_service_key )
2016-12-21 22:30:54 +00:00
2017-06-28 20:23:21 +00:00
tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )
2016-09-21 19:54:04 +00:00
2016-10-26 20:45:34 +00:00
self._file_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, file_service.GetName(), self.FileButtonHit )
2016-09-21 19:54:04 +00:00
self._file_repo_button.SetMinSize( ( 20, -1 ) )
2016-10-26 20:45:34 +00:00
self._tag_repo_button = ClientGUICommon.BetterButton( self._dropdown_window, tag_service.GetName(), self.TagButtonHit )
2016-09-21 19:54:04 +00:00
self._tag_repo_button.SetMinSize( ( 20, -1 ) )
2019-01-30 22:14:54 +00:00
self._favourites_list = self._InitFavouritesList()
self.RefreshFavouriteTags()
self._dropdown_notebook.AddPage( self._favourites_list, 'favourites', False )
#
HG.client_controller.sub( self, 'RefreshFavouriteTags', 'notify_new_favourite_tags' )
2016-09-21 19:54:04 +00:00
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
2017-06-28 20:23:21 +00:00
file_service = HG.client_controller.services_manager.GetService( self._file_service_key )
2016-09-21 19:54:04 +00:00
name = file_service.GetName()
self._file_repo_button.SetLabelText( name )
2018-02-14 21:47:18 +00:00
self._SetListDirty()
2016-09-21 19:54:04 +00:00
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
2018-03-28 21:55:58 +00:00
self._search_results_list.SetTagService( self._tag_service_key )
2016-09-21 19:54:04 +00:00
2017-06-28 20:23:21 +00:00
tag_service = tag_service = HG.client_controller.services_manager.GetService( self._tag_service_key )
2016-09-21 19:54:04 +00:00
name = tag_service.GetName()
self._tag_repo_button.SetLabelText( name )
2019-01-30 22:14:54 +00:00
self._search_text_for_current_cache = None
2016-09-21 19:54:04 +00:00
2018-02-14 21:47:18 +00:00
self._SetListDirty()
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
def _InitFavouritesList( self ):
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
raise NotImplementedError()
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
def _SetResultsToList( self, results ):
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
self._search_results_list.SetPredicates( results )
2016-09-21 19:54:04 +00:00
2016-10-26 20:45:34 +00:00
def FileButtonHit( self ):
2017-06-28 20:23:21 +00:00
services_manager = HG.client_controller.services_manager
2016-10-26 20:45:34 +00:00
services = []
2016-12-21 22:30:54 +00:00
2016-10-26 20:45:34 +00:00
services.append( services_manager.GetService( CC.LOCAL_FILE_SERVICE_KEY ) )
services.append( services_manager.GetService( CC.TRASH_SERVICE_KEY ) )
2019-05-22 22:35:06 +00:00
if HG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
services.append( services_manager.GetService( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
2016-10-26 20:45:34 +00:00
services.extend( services_manager.GetServices( ( HC.FILE_REPOSITORY, ) ) )
2017-11-22 21:03:07 +00:00
advanced_mode = HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
2019-04-24 22:18:50 +00:00
if advanced_mode and self._allow_all_known_files:
2017-11-22 21:03:07 +00:00
services.append( services_manager.GetService( CC.COMBINED_FILE_SERVICE_KEY ) )
2016-10-26 20:45:34 +00:00
menu = wx.Menu()
2016-12-21 22:30:54 +00:00
for service in services:
2017-03-29 19:39:34 +00:00
ClientGUIMenus.AppendMenuItem( self, menu, service.GetName(), 'Change the current file domain to ' + service.GetName() + '.', self._ChangeFileService, service.GetServiceKey() )
2016-12-21 22:30:54 +00:00
2016-10-26 20:45:34 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.PopupMenu( self._file_repo_button, menu )
2016-10-26 20:45:34 +00:00
2019-01-30 22:14:54 +00:00
def RefreshFavouriteTags( self ):
favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )
favourite_tags.sort()
predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ]
self._favourites_list.SetPredicates( predicates )
2016-10-26 20:45:34 +00:00
def SetFileService( self, file_service_key ):
self._ChangeFileService( file_service_key )
def SetTagService( self, tag_service_key ):
self._ChangeTagService( tag_service_key )
def TagButtonHit( self ):
2016-09-21 19:54:04 +00:00
2017-06-28 20:23:21 +00:00
services_manager = HG.client_controller.services_manager
2016-09-21 19:54:04 +00:00
services = []
2016-12-21 22:30:54 +00:00
2016-09-21 19:54:04 +00:00
services.append( services_manager.GetService( CC.LOCAL_TAG_SERVICE_KEY ) )
services.extend( services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) )
2016-12-21 22:30:54 +00:00
services.append( services_manager.GetService( CC.COMBINED_TAG_SERVICE_KEY ) )
2016-09-21 19:54:04 +00:00
menu = wx.Menu()
2016-12-21 22:30:54 +00:00
for service in services:
2017-03-29 19:39:34 +00:00
ClientGUIMenus.AppendMenuItem( self, menu, service.GetName(), 'Change the current tag domain to ' + service.GetName() + '.', self._ChangeTagService, service.GetServiceKey() )
2016-12-21 22:30:54 +00:00
2016-09-21 19:54:04 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.PopupMenu( self._tag_repo_button, menu )
2016-09-21 19:54:04 +00:00
class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
2019-04-24 22:18:50 +00:00
def __init__( self, parent, page_key, file_search_context, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True ):
2016-09-21 19:54:04 +00:00
file_service_key = file_search_context.GetFileServiceKey()
tag_service_key = file_search_context.GetTagServiceKey()
AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )
2019-04-24 22:18:50 +00:00
self._allow_all_known_files = allow_all_known_files
2016-09-21 19:54:04 +00:00
self._media_callable = media_callable
self._page_key = page_key
2019-04-03 22:45:57 +00:00
self._under_construction_or_predicate = None
2016-09-21 19:54:04 +00:00
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() )
2018-01-03 22:37:30 +00:00
self._include_current_tags.SetToolTip( 'select whether to include current tags in the search' )
2016-09-21 19:54:04 +00:00
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() )
2018-01-03 22:37:30 +00:00
self._include_pending_tags.SetToolTip( 'select whether to include pending tags in the search' )
2016-09-21 19:54:04 +00:00
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 )
2018-01-03 22:37:30 +00:00
self._synchronised.SetToolTip( 'select whether to renew the search as soon as a new predicate is entered' )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
self._or_cancel = ClientGUICommon.BetterBitmapButton( self._dropdown_window, CC.GlobalBMPs.delete, self._CancelORConstruction )
self._or_cancel.SetToolTip( 'Cancel OR Predicate construction.' )
self._or_cancel.Hide()
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
self._or_rewind = ClientGUICommon.BetterBitmapButton( self._dropdown_window, CC.GlobalBMPs.previous, self._RewindORConstruction )
self._or_rewind.SetToolTip( 'Rewind OR Predicate construction.' )
self._or_rewind.Hide()
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
self._include_unusual_predicate_types = include_unusual_predicate_types
2019-04-03 22:45:57 +00:00
2016-09-21 19:54:04 +00:00
button_hbox_1 = wx.BoxSizer( wx.HORIZONTAL )
2018-01-03 22:37:30 +00:00
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 )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
sync_button_hbox = wx.BoxSizer( wx.HORIZONTAL )
sync_button_hbox.Add( self._synchronised, CC.FLAGS_EXPAND_BOTH_WAYS )
2019-04-17 21:51:50 +00:00
sync_button_hbox.Add( self._or_cancel, CC.FLAGS_VCENTER )
sync_button_hbox.Add( self._or_rewind, CC.FLAGS_VCENTER )
2019-04-10 22:50:53 +00:00
2016-09-21 19:54:04 +00:00
button_hbox_2 = wx.BoxSizer( wx.HORIZONTAL )
2018-01-03 22:37:30 +00:00
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 )
2016-09-21 19:54:04 +00:00
vbox = wx.BoxSizer( wx.VERTICAL )
2018-01-03 22:37:30 +00:00
vbox.Add( button_hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
2019-04-10 22:50:53 +00:00
vbox.Add( sync_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
2018-01-03 22:37:30 +00:00
vbox.Add( button_hbox_2, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
2018-03-28 21:55:58 +00:00
vbox.Add( self._dropdown_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
2016-09-21 19:54:04 +00:00
self._dropdown_window.SetSizer( vbox )
2017-05-10 21:33:58 +00:00
HG.client_controller.sub( self, 'SetSynchronisedWait', 'synchronised_wait_switch' )
2016-09-21 19:54:04 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.sub( self, 'IncludeCurrent', 'notify_include_current' )
HG.client_controller.sub( self, 'IncludePending', 'notify_include_pending' )
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
def _BroadcastChoices( self, predicates, shift_down ):
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
or_pred_in_broadcast = self._under_construction_or_predicate is not None and self._under_construction_or_predicate in predicates
2019-04-03 22:45:57 +00:00
if shift_down:
if self._under_construction_or_predicate is None:
self._under_construction_or_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, predicates )
else:
2019-04-10 22:50:53 +00:00
if or_pred_in_broadcast:
predicates.remove( self._under_construction_or_predicate )
2019-04-03 22:45:57 +00:00
or_preds = list( self._under_construction_or_predicate.GetValue() )
or_preds.extend( [ predicate for predicate in predicates if predicate not in or_preds ] )
self._under_construction_or_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds )
else:
2019-04-10 22:50:53 +00:00
if self._under_construction_or_predicate is not None and not or_pred_in_broadcast:
2019-04-03 22:45:57 +00:00
or_preds = list( self._under_construction_or_predicate.GetValue() )
or_preds.extend( [ predicate for predicate in predicates if predicate not in or_preds ] )
predicates = { ClientSearch.Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds ) }
2019-04-10 22:50:53 +00:00
if or_pred_in_broadcast:
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
or_preds = list( self._under_construction_or_predicate.GetValue() )
if len( or_preds ) == 1:
predicates.remove( self._under_construction_or_predicate )
predicates.extend( or_preds )
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
self._under_construction_or_predicate = None
2019-04-03 22:45:57 +00:00
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
self._UpdateORButtons()
2018-05-23 21:05:06 +00:00
self._ClearInput()
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
def _BroadcastCurrentText( self, shift_down ):
2016-09-21 19:54:04 +00:00
2018-04-25 22:07:52 +00:00
( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = self._ParseSearchText()
2016-09-21 19:54:04 +00:00
2019-06-05 19:42:39 +00:00
( namespace, subtag ) = HydrusTags.SplitTag( search_text )
if namespace != '' and subtag in ( '', '*' ):
2016-09-21 19:54:04 +00:00
2019-06-05 19:42:39 +00:00
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_NAMESPACE, namespace, inclusive )
2016-09-21 19:54:04 +00:00
2019-06-05 19:42:39 +00:00
else:
2016-09-21 19:54:04 +00:00
2019-06-05 19:42:39 +00:00
try:
HydrusTags.CheckTagNotEmpty( search_text )
except HydrusExceptions.SizeException:
return
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
self._BroadcastChoices( { entry_predicate }, shift_down )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
def _CancelORConstruction( self ):
self._under_construction_or_predicate = None
self._UpdateORButtons()
self._ClearInput()
2016-09-21 19:54:04 +00:00
def _ChangeFileService( self, file_service_key ):
AutoCompleteDropdownTags._ChangeFileService( self, file_service_key )
self._file_search_context.SetFileServiceKey( file_service_key )
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'change_file_service', self._page_key, file_service_key )
2016-09-21 19:54:04 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'refresh_query', self._page_key )
2016-09-21 19:54:04 +00:00
def _ChangeTagService( self, tag_service_key ):
AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )
self._file_search_context.SetTagServiceKey( tag_service_key )
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'change_tag_service', self._page_key, tag_service_key )
2016-09-21 19:54:04 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'refresh_query', self._page_key )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
def _HandleEscape( self ):
if self._under_construction_or_predicate is not None and self._text_ctrl.GetValue() == '':
self._CancelORConstruction()
return True
else:
return AutoCompleteDropdown._HandleEscape( self )
2018-03-28 21:55:58 +00:00
def _InitFavouritesList( self ):
2018-05-23 21:05:06 +00:00
favs_list = ClientGUIListBoxes.ListBoxTagsACRead( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
return favs_list
def _InitSearchResultsList( self ):
2018-05-23 21:05:06 +00:00
return ClientGUIListBoxes.ListBoxTagsACRead( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )
2016-09-21 19:54:04 +00:00
def _ParseSearchText( self ):
raw_entry = self._text_ctrl.GetValue()
if raw_entry.startswith( '-' ):
inclusive = False
2017-03-08 23:23:12 +00:00
entry_text = raw_entry[1:]
2016-09-21 19:54:04 +00:00
else:
inclusive = True
2017-03-08 23:23:12 +00:00
entry_text = raw_entry
2016-09-21 19:54:04 +00:00
2017-03-08 23:23:12 +00:00
tag = HydrusTags.CleanTag( entry_text )
2017-02-01 21:11:17 +00:00
2017-03-08 23:23:12 +00:00
explicit_wildcard = '*' in entry_text
2016-09-21 19:54:04 +00:00
2017-03-08 23:23:12 +00:00
search_text = ClientSearch.ConvertEntryTextToSearchText( entry_text )
2016-09-21 19:54:04 +00:00
2017-03-08 23:23:12 +00:00
if explicit_wildcard:
cache_text = None
2016-09-21 19:54:04 +00:00
2017-03-08 23:23:12 +00:00
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_WILDCARD, search_text, inclusive )
2016-09-21 19:54:04 +00:00
else:
2017-03-08 23:23:12 +00:00
cache_text = search_text[:-1] # take off the trailing '*' for the cache text
2019-04-17 21:51:50 +00:00
siblings_manager = HG.client_controller.tag_siblings_manager
2017-03-08 23:23:12 +00:00
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 )
2016-09-21 19:54:04 +00:00
2018-04-25 22:07:52 +00:00
return ( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
def _RewindORConstruction( self ):
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
if self._under_construction_or_predicate is not None:
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
or_preds = self._under_construction_or_predicate.GetValue()
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
if len( or_preds ) <= 1:
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
self._CancelORConstruction()
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
return
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
or_preds = or_preds[:-1]
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
self._under_construction_or_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_OR_CONTAINER, or_preds )
2019-04-03 22:45:57 +00:00
2019-04-10 22:50:53 +00:00
self._UpdateORButtons()
self._ClearInput()
2019-04-03 22:45:57 +00:00
2019-01-30 22:14:54 +00:00
def _StartResultsFetchJob( self, job_key ):
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
parsed_search_text = self._ParseSearchText()
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
HG.client_controller.CallToThread( ReadFetch, self, job_key, self.SetFetchedResults, parsed_search_text, self._media_callable, self._file_search_context, self._synchronised.IsOn(), self._include_unusual_predicate_types, self._initial_matches_fetched, self._search_text_for_current_cache, self._cached_results, self._under_construction_or_predicate )
2016-09-21 19:54:04 +00:00
def _ShouldTakeResponsibilityForEnter( self ):
2019-01-30 22:14:54 +00:00
( raw_entry, inclusive, search_text, explicit_wildcard, cache_text, entry_predicate ) = self._ParseSearchText()
sitting_on_empty = raw_entry == ''
2016-09-21 19:54:04 +00:00
# when the user has quickly typed something in and the results are not yet in
2019-01-30 22:14:54 +00:00
p1 = not sitting_on_empty and self._last_fetched_search_text != search_text
return p1
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
def _TakeResponsibilityForEnter( self, shift_down ):
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
self._BroadcastCurrentText( shift_down )
2016-09-21 19:54:04 +00:00
2019-04-10 22:50:53 +00:00
def _UpdateORButtons( self ):
layout_needed = False
if self._under_construction_or_predicate is None:
if self._or_cancel.IsShown():
self._or_cancel.Hide()
layout_needed = True
if self._or_rewind.IsShown():
self._or_rewind.Hide()
layout_needed = True
else:
or_preds = self._under_construction_or_predicate.GetValue()
if len( or_preds ) > 1:
if not self._or_rewind.IsShown():
self._or_rewind.Show()
layout_needed = True
else:
if self._or_rewind.IsShown():
self._or_rewind.Hide()
layout_needed = True
if not self._or_cancel.IsShown():
self._or_cancel.Show()
layout_needed = True
if layout_needed:
self._dropdown_window.Layout()
2016-09-21 19:54:04 +00:00
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 )
2018-02-14 21:47:18 +00:00
self._SetListDirty()
2017-01-18 22:52:39 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'refresh_query', self._page_key )
2017-01-18 22:52:39 +00:00
2016-09-21 19:54:04 +00:00
def IncludePending( self, page_key, value ):
if page_key == self._page_key:
self._file_search_context.SetIncludePendingTags( value )
2018-02-14 21:47:18 +00:00
self._SetListDirty()
2017-01-18 22:52:39 +00:00
2017-05-10 21:33:58 +00:00
HG.client_controller.pub( 'refresh_query', self._page_key )
2017-01-18 22:52:39 +00:00
2016-09-21 19:54:04 +00:00
2019-04-24 22:18:50 +00:00
def IsSynchronised( self ):
return self._synchronised.IsOn()
2019-01-30 22:14:54 +00:00
def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results, next_search_is_probably_fast ):
if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
AutoCompleteDropdownTags.SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results )
self._next_search_is_probably_fast = next_search_is_probably_fast
num_chars = len( self._text_ctrl.GetValue() )
if num_chars == 0:
# refresh system preds after five mins
self._ScheduleListRefresh( 300 )
2016-09-21 19:54:04 +00:00
def SetSynchronisedWait( self, page_key ):
2019-04-24 22:18:50 +00:00
if page_key == self._page_key:
self._synchronised.EventButton( None )
2016-09-21 19:54:04 +00:00
class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
2019-06-26 21:27:18 +00:00
def __init__( self, parent, chosen_tag_callable, expand_parents, file_service_key, tag_service_key, null_entry_callable = None, tag_service_key_changed_callable = None, show_paste_button = False ):
2016-09-21 19:54:04 +00:00
self._chosen_tag_callable = chosen_tag_callable
self._expand_parents = expand_parents
self._null_entry_callable = null_entry_callable
2018-11-21 22:22:36 +00:00
self._tag_service_key_changed_callable = tag_service_key_changed_callable
2016-09-21 19:54:04 +00:00
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
2017-05-10 21:33:58 +00:00
if tag_service_key == CC.LOCAL_TAG_SERVICE_KEY:
file_service_key = CC.LOCAL_FILE_SERVICE_KEY
2016-09-21 19:54:04 +00:00
AutoCompleteDropdownTags.__init__( self, parent, file_service_key, tag_service_key )
2019-06-26 21:27:18 +00:00
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.paste, self._Paste )
self._paste_button.SetToolTip( 'Paste from the clipboard and quick-enter as if you had typed. This can take multiple newline-separated tags.' )
if not show_paste_button:
self._paste_button.Hide()
self._text_input_hbox.Add( self._paste_button, CC.FLAGS_VCENTER )
2016-09-21 19:54:04 +00:00
vbox = wx.BoxSizer( wx.VERTICAL )
hbox = wx.BoxSizer( wx.HORIZONTAL )
2018-01-03 22:37:30 +00:00
hbox.Add( self._file_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
hbox.Add( self._tag_repo_button, CC.FLAGS_EXPAND_BOTH_WAYS )
2016-09-21 19:54:04 +00:00
2018-01-03 22:37:30 +00:00
vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
2018-03-28 21:55:58 +00:00
vbox.Add( self._dropdown_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
2016-09-21 19:54:04 +00:00
self._dropdown_window.SetSizer( vbox )
2019-04-03 22:45:57 +00:00
def _BroadcastChoices( self, predicates, shift_down ):
2016-09-21 19:54:04 +00:00
tags = { predicate.GetValue() for predicate in predicates }
if len( tags ) > 0:
self._chosen_tag_callable( tags )
2018-05-23 21:05:06 +00:00
self._ClearInput()
2016-09-21 19:54:04 +00:00
def _ParseSearchText( self ):
raw_entry = self._text_ctrl.GetValue()
2017-02-01 21:11:17 +00:00
tag = HydrusTags.CleanTag( raw_entry )
2017-03-08 23:23:12 +00:00
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
2016-09-21 19:54:04 +00:00
2017-02-01 21:11:17 +00:00
entry_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag )
2016-09-21 19:54:04 +00:00
2019-04-17 21:51:50 +00:00
siblings_manager = HG.client_controller.tag_siblings_manager
2016-09-21 19:54:04 +00:00
2017-02-01 21:11:17 +00:00
sibling = siblings_manager.GetSibling( self._tag_service_key, tag )
2016-09-21 19:54:04 +00:00
if sibling is not None:
sibling_predicate = ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, sibling )
else:
sibling_predicate = None
2018-04-25 22:07:52 +00:00
return ( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate )
2016-09-21 19:54:04 +00:00
2019-04-03 22:45:57 +00:00
def _BroadcastCurrentText( self, shift_down ):
2016-09-21 19:54:04 +00:00
2018-04-25 22:07:52 +00:00
( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()
2016-09-21 19:54:04 +00:00
try:
HydrusTags.CheckTagNotEmpty( search_text )
except HydrusExceptions.SizeException:
return
2019-04-03 22:45:57 +00:00
self._BroadcastChoices( { entry_predicate }, shift_down )
2016-09-21 19:54:04 +00:00
2018-11-21 22:22:36 +00:00
def _ChangeTagService( self, tag_service_key ):
AutoCompleteDropdownTags._ChangeTagService( self, tag_service_key )
if self._tag_service_key_changed_callable is not None:
self._tag_service_key_changed_callable( tag_service_key )
2018-03-28 21:55:58 +00:00
def _InitFavouritesList( self ):
2018-05-23 21:05:06 +00:00
favs_list = ClientGUIListBoxes.ListBoxTagsACWrite( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )
2018-03-28 21:55:58 +00:00
return favs_list
def _InitSearchResultsList( self ):
2016-09-21 19:54:04 +00:00
2018-05-23 21:05:06 +00:00
return ClientGUIListBoxes.ListBoxTagsACWrite( self._dropdown_notebook, self.BroadcastChoices, self._tag_service_key, height_num_chars = self._list_height_num_chars )
2016-09-21 19:54:04 +00:00
2019-06-26 21:27:18 +00:00
def _Paste( self ):
try:
raw_text = HG.client_controller.GetClipboardText()
except HydrusExceptions.DataMissing as e:
wx.MessageBox( str( e ) )
return
try:
tags = [ text for text in HydrusText.DeserialiseNewlinedTexts( raw_text ) ]
tags = HydrusTags.CleanTags( tags )
entry_predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in tags ]
if len( entry_predicates ) > 0:
shift_down = False
self._BroadcastChoices( entry_predicates, shift_down )
except:
wx.MessageBox( 'I could not understand what was in the clipboard' )
raise
2019-01-30 22:14:54 +00:00
def _ShouldTakeResponsibilityForEnter( self ):
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
( raw_entry, search_text, cache_text, entry_predicate, sibling_predicate ) = self._ParseSearchText()
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
sitting_on_empty = raw_entry == ''
2016-09-21 19:54:04 +00:00
# when the user has quickly typed something in and the results are not yet in
2019-01-30 22:14:54 +00:00
p1 = not sitting_on_empty and self._last_fetched_search_text != search_text
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
# when the text ctrl is empty, we are looking at search results, and we want to push a None to the parent dialog
2016-09-21 19:54:04 +00:00
2019-01-30 22:14:54 +00:00
p2 = sitting_on_empty and self._dropdown_notebook.GetCurrentPage() == self._search_results_list
2016-09-21 19:54:04 +00:00
return p1 or p2
2019-01-30 22:14:54 +00:00
def _StartResultsFetchJob( self, job_key ):
parsed_search_text = self._ParseSearchText()
HG.client_controller.CallToThread( WriteFetch, self, job_key, self.SetFetchedResults, parsed_search_text, self._file_service_key, self._tag_service_key, self._expand_parents, self._search_text_for_current_cache, self._cached_results )
2019-04-03 22:45:57 +00:00
def _TakeResponsibilityForEnter( self, shift_down ):
2016-09-21 19:54:04 +00:00
2018-03-28 21:55:58 +00:00
if self._text_ctrl.GetValue() == '' and self._dropdown_notebook.GetCurrentPage() == self._search_results_list:
2016-09-21 19:54:04 +00:00
if self._null_entry_callable is not None:
self._null_entry_callable()
else:
2019-04-03 22:45:57 +00:00
self._BroadcastCurrentText( shift_down )
2016-09-21 19:54:04 +00:00
2016-12-21 22:30:54 +00:00
2018-04-05 01:22:26 +00:00
def RefreshFavouriteTags( self ):
favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )
favourite_tags.sort()
predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ]
2019-04-17 21:51:50 +00:00
parents_manager = HG.client_controller.tag_parents_manager
2018-04-05 01:22:26 +00:00
predicates = parents_manager.ExpandPredicates( CC.COMBINED_TAG_SERVICE_KEY, predicates )
self._favourites_list.SetPredicates( predicates )
2019-01-30 22:14:54 +00:00
def SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results, next_search_is_probably_fast ):
if self._current_fetch_job_key is not None and self._current_fetch_job_key.GetKey() == job_key.GetKey():
AutoCompleteDropdownTags.SetFetchedResults( self, job_key, search_text, search_text_for_cache, cached_results, results )
self._next_search_is_probably_fast = next_search_is_probably_fast