2886 lines
97 KiB
Python
Executable File
2886 lines
97 KiB
Python
Executable File
import collections
|
|
import HydrusConstants as HC
|
|
import ClientConstants as CC
|
|
import ClientGUIMixins
|
|
import os
|
|
import random
|
|
import time
|
|
import traceback
|
|
import wx
|
|
import wx.combo
|
|
import wx.richtext
|
|
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
|
|
from wx.lib.mixins.listctrl import ColumnSorterMixin
|
|
|
|
ID_TIMER_ANIMATED = wx.NewId()
|
|
ID_TIMER_SLIDESHOW = wx.NewId()
|
|
ID_TIMER_MEDIA_INFO_DISPLAY = wx.NewId()
|
|
ID_TIMER_DROPDOWN_HIDE = wx.NewId()
|
|
ID_TIMER_AC_LAG = wx.NewId()
|
|
|
|
# Zooms
|
|
|
|
ZOOMINS = [ 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 3.0, 5.0, 10.0, 20.0 ]
|
|
ZOOMOUTS = [ 20.0, 10.0, 5.0, 3.0, 2.0, 1.5, 1.2, 1.1, 1.0, 0.9, 0.8, 0.7, 0.5, 0.3, 0.2, 0.15, 0.1, 0.05, 0.01 ]
|
|
|
|
# Sizer Flags
|
|
|
|
FLAGS_NONE = wx.SizerFlags( 0 )
|
|
|
|
FLAGS_SMALL_INDENT = wx.SizerFlags( 0 ).Border( wx.ALL, 2 )
|
|
|
|
FLAGS_EXPAND_PERPENDICULAR = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Expand()
|
|
FLAGS_EXPAND_BOTH_WAYS = wx.SizerFlags( 2 ).Border( wx.ALL, 2 ).Expand()
|
|
|
|
FLAGS_EXPAND_SIZER_PERPENDICULAR = wx.SizerFlags( 0 ).Expand()
|
|
FLAGS_EXPAND_SIZER_BOTH_WAYS = wx.SizerFlags( 2 ).Expand()
|
|
|
|
FLAGS_BUTTON_SIZERS = wx.SizerFlags( 0 ).Align( wx.ALIGN_RIGHT )
|
|
FLAGS_LONE_BUTTON = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_RIGHT )
|
|
|
|
FLAGS_MIXED = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
|
|
class AnimatedStaticTextTimestamp( wx.StaticText ):
|
|
|
|
def __init__( self, parent, prefix, rendering_function, timestamp, suffix ):
|
|
|
|
self._prefix = prefix
|
|
self._rendering_function = rendering_function
|
|
self._timestamp = timestamp
|
|
self._suffix = suffix
|
|
|
|
self._last_tick = int( time.time() )
|
|
|
|
wx.StaticText.__init__( self, parent, label = self._prefix + self._rendering_function( self._timestamp ) + self._suffix )
|
|
|
|
HC.pubsub.sub( self, 'Tick', 'animated_tick' )
|
|
|
|
|
|
def Tick( self ):
|
|
|
|
update = False
|
|
|
|
now = int( time.time() )
|
|
|
|
difference = abs( now - self._timestamp )
|
|
|
|
if difference < 3600: update = True
|
|
elif difference < 3600 * 24 and now - self._last_tick > 60: update = True
|
|
elif now - self._last_tick > 3600: update = True
|
|
|
|
if update:
|
|
|
|
self.SetLabel( self._prefix + self._rendering_function( self._timestamp ) + self._suffix )
|
|
|
|
wx.PostEvent( self.GetEventHandler(), wx.SizeEvent() )
|
|
|
|
|
|
|
|
# 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.TextCtrl ):
|
|
|
|
def __init__( self, parent ):
|
|
|
|
wx.TextCtrl.__init__( self, parent, style=wx.TE_PROCESS_ENTER )
|
|
|
|
self._dropdown_window = wx.PopupWindow( self, flags = wx.BORDER_RAISED )
|
|
self._dropdown_window.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
|
|
|
|
self._first_letters = ''
|
|
self._cached_results = self._InitCachedResults()
|
|
|
|
self._dropdown_list = self._InitDropDownList()
|
|
|
|
self.Bind( wx.EVT_SET_FOCUS, self.EventSetFocus )
|
|
self.Bind( wx.EVT_KILL_FOCUS, self.EventKillFocus )
|
|
|
|
self.Bind( wx.EVT_TEXT, self.EventText, self )
|
|
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown, self )
|
|
|
|
self.Bind( wx.EVT_MOVE, self.EventMove )
|
|
self.Bind( wx.EVT_SIZE, self.EventMove )
|
|
|
|
self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
|
|
|
|
self.Bind( wx.EVT_TIMER, self.EventDropdownHideTimer, id = ID_TIMER_DROPDOWN_HIDE )
|
|
self.Bind( wx.EVT_TIMER, self.EventLagTimer, id = ID_TIMER_AC_LAG )
|
|
|
|
self._move_hide_timer = wx.Timer( self, id = ID_TIMER_DROPDOWN_HIDE )
|
|
self._lag_timer = wx.Timer( self, id = ID_TIMER_AC_LAG )
|
|
|
|
tlp = self.GetTopLevelParent()
|
|
|
|
tlp.Bind( wx.EVT_MOVE, self.EventMove )
|
|
|
|
wx.CallAfter( self._UpdateList )
|
|
|
|
|
|
def _BroadcastChoice( self, predicate ): pass
|
|
|
|
def BroadcastChoice( self, predicate ):
|
|
|
|
self._BroadcastChoice( predicate )
|
|
|
|
self.Clear()
|
|
|
|
wx.CallAfter( self._UpdateList )
|
|
|
|
|
|
def _HideDropdown( self ): self._dropdown_window.Show( False )
|
|
|
|
def _ShowDropdownIfFocussed( self ):
|
|
|
|
if self.GetTopLevelParent().IsActive() and wx.Window.FindFocus() == self and len( self._dropdown_list ) > 0:
|
|
|
|
( my_width, my_height ) = self.GetSize()
|
|
|
|
#self._dropdown_list.Show()
|
|
|
|
self._dropdown_window.Fit()
|
|
|
|
self._dropdown_window.SetSize( ( my_width, -1 ) )
|
|
|
|
self._dropdown_window.Layout()
|
|
|
|
self._dropdown_window.SetPosition( self.ClientToScreenXY( -2, my_height - 2 ) )
|
|
|
|
self._dropdown_window.Show()
|
|
|
|
|
|
|
|
def _UpdateList( self ): pass
|
|
|
|
def EventDropdownHideTimer( self, event ):
|
|
|
|
try: self._ShowDropdownIfFocussed()
|
|
except: pass
|
|
|
|
|
|
def EventKeyDown( self, event ):
|
|
|
|
if event.KeyCode in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ) and self.GetValue() == '' and len( self._dropdown_list ) == 0: self._BroadcastChoice( None )
|
|
elif event.KeyCode == wx.WXK_ESCAPE: self.GetTopLevelParent().SetFocus()
|
|
elif event.KeyCode in ( wx.WXK_UP, wx.WXK_NUMPAD_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ) and self.GetValue() == '' and len( self._dropdown_list ) == 0:
|
|
|
|
if event.KeyCode in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ): id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'select_up' )
|
|
elif event.KeyCode in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ): id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'select_down' )
|
|
|
|
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
|
|
|
|
self.ProcessEvent( new_event )
|
|
|
|
else: self._dropdown_list.ProcessEvent( event )
|
|
|
|
|
|
def EventKillFocus( self, event ):
|
|
|
|
self._HideDropdown()
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventLagTimer( self, event ): self._UpdateList()
|
|
|
|
def EventMouseWheel( self, event ):
|
|
|
|
if self.GetValue() == '' and len( self._dropdown_list ) == 0:
|
|
|
|
if event.GetWheelRotation() > 0: id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'select_up' )
|
|
else: id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'select_down' )
|
|
|
|
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
|
|
|
|
self.ProcessEvent( new_event )
|
|
|
|
else:
|
|
|
|
if event.CmdDown():
|
|
|
|
key_event = wx.KeyEvent( wx.EVT_KEY_DOWN.typeId )
|
|
|
|
if event.GetWheelRotation() > 0: key_event.m_keyCode = wx.WXK_UP
|
|
else: key_event.m_keyCode = wx.WXK_DOWN
|
|
|
|
self._dropdown_list.ProcessEvent( key_event )
|
|
|
|
else:
|
|
|
|
# for some reason, the scrolledwindow list doesn't process scroll events properly when in a popupwindow
|
|
# so let's just tell it to scroll manually
|
|
|
|
( start_x, start_y ) = self._dropdown_list.GetViewStart()
|
|
|
|
if event.GetWheelRotation() > 0: self._dropdown_list.Scroll( -1, start_y - 3 )
|
|
else: self._dropdown_list.Scroll( -1, start_y + 3 )
|
|
|
|
|
|
|
|
|
|
def EventMove( self, event ):
|
|
|
|
try:
|
|
|
|
self._HideDropdown()
|
|
|
|
num_chars = len( self.GetValue() )
|
|
|
|
if num_chars == 0: lag = 0
|
|
elif num_chars == 1: lag = 400
|
|
elif num_chars == 2: lag = 250
|
|
else: lag = 100
|
|
|
|
self._move_hide_timer.Start( lag, wx.TIMER_ONE_SHOT )
|
|
|
|
except wx.PyDeadObjectError: pass
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventSetFocus( self, event ):
|
|
|
|
self._ShowDropdownIfFocussed()
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventText( self, event ):
|
|
|
|
self._lag_timer.Start( 100, wx.TIMER_ONE_SHOT )
|
|
|
|
|
|
class AutoCompleteDropdownContacts( AutoCompleteDropdown ):
|
|
|
|
def __init__( self, parent, compose_key, identity ):
|
|
|
|
AutoCompleteDropdown.__init__( self, parent )
|
|
|
|
self._compose_key = compose_key
|
|
self._identity = identity
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( self._dropdown_list, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._dropdown_window.SetSizer( vbox )
|
|
|
|
|
|
def _BroadcastChoice( self, contact_name ): HC.pubsub.pub( 'add_contact', self._compose_key, contact_name )
|
|
|
|
def _GenerateMatches( self ):
|
|
|
|
num_first_letters = 1
|
|
|
|
entry = self.GetValue()
|
|
|
|
if entry == '':
|
|
|
|
self._first_letters = ''
|
|
|
|
matches = []
|
|
|
|
else:
|
|
|
|
if len( entry ) >= num_first_letters:
|
|
|
|
if entry[ : num_first_letters ] != self._first_letters:
|
|
|
|
self._first_letters = entry[ : num_first_letters ]
|
|
|
|
self._cached_results = wx.GetApp().Read( 'autocomplete_contacts', entry, name_to_exclude = self._identity.GetName() )
|
|
|
|
|
|
matches = self._cached_results.GetMatches( entry )
|
|
|
|
else: matches = []
|
|
|
|
|
|
return matches
|
|
|
|
|
|
def _InitCachedResults( self ): return CC.AutocompleteMatches( [] )
|
|
|
|
def _InitDropDownList( self ): return ListBoxMessagesActiveOnly( self._dropdown_window, self.BroadcastChoice )
|
|
|
|
def _UpdateList( self ):
|
|
|
|
matches = self._GenerateMatches()
|
|
|
|
# this obv needs to be SetValues or whatever
|
|
self._dropdown_list.SetTexts( matches )
|
|
|
|
if len( matches ) > 0: self._ShowDropdownIfFocussed()
|
|
else: self._HideDropdown()
|
|
|
|
|
|
class AutoCompleteDropdownMessageTerms( AutoCompleteDropdown ):
|
|
|
|
def __init__( self, parent, page_key, identity ):
|
|
|
|
AutoCompleteDropdown.__init__( self, parent )
|
|
|
|
self._page_key = page_key
|
|
self._identity = identity
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( self._dropdown_list, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._dropdown_window.SetSizer( vbox )
|
|
|
|
|
|
def _BroadcastChoice( self, predicate ): HC.pubsub.pub( 'add_predicate', self._page_key, predicate )
|
|
|
|
def _InitCachedResults( self ): return CC.AutocompleteMatchesCounted( {} )
|
|
|
|
def _InitDropDownList( self ): return ListBoxMessagesActiveOnly( self._dropdown_window, self.BroadcastChoice )
|
|
|
|
def _GenerateMatches( self ):
|
|
|
|
entry = self.GetValue()
|
|
|
|
if entry.startswith( '-' ): search_term = entry[1:]
|
|
else: search_term = entry
|
|
|
|
if search_term == '': matches = wx.GetApp().Read( 'message_system_predicates', self._identity )
|
|
else: matches = [ ( entry, None ) ]
|
|
|
|
return matches
|
|
|
|
|
|
def _UpdateList( self ):
|
|
|
|
matches = self._GenerateMatches()
|
|
|
|
self._dropdown_list.SetTerms( matches )
|
|
|
|
if len( matches ) > 0: self._ShowDropdownIfFocussed()
|
|
else: self._HideDropdown()
|
|
|
|
|
|
class AutoCompleteDropdownTags( AutoCompleteDropdown ):
|
|
|
|
def __init__( self, parent, file_service_identifier, tag_service_identifier ):
|
|
|
|
AutoCompleteDropdown.__init__( self, parent )
|
|
|
|
self._options = wx.GetApp().Read( 'options' )
|
|
|
|
self._current_namespace = ''
|
|
self._current_matches = []
|
|
|
|
self._file_service_identifier = file_service_identifier
|
|
self._tag_service_identifier = tag_service_identifier
|
|
|
|
if self._file_service_identifier == CC.NULL_SERVICE_IDENTIFIER: name = 'all known files'
|
|
else: name = self._file_service_identifier.GetName()
|
|
|
|
self._file_repo_button = wx.Button( self._dropdown_window, label = name )
|
|
self._file_repo_button.Bind( wx.EVT_BUTTON, self.EventFileButton )
|
|
|
|
if self._tag_service_identifier == CC.NULL_SERVICE_IDENTIFIER: name = 'all known tags'
|
|
else: name = self._tag_service_identifier.GetName()
|
|
|
|
self._tag_repo_button = wx.Button( self._dropdown_window, label = name )
|
|
self._tag_repo_button.Bind( wx.EVT_BUTTON, self.EventTagButton )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
|
|
def _InitCachedResults( self ): return CC.AutocompleteMatchesCounted( {} )
|
|
|
|
def _InitDropDownList( self ): return TagsBoxActiveOnly( self._dropdown_window, self.BroadcastChoice )
|
|
|
|
def _ShowDropdownIfFocussed( self ):
|
|
|
|
# don't know why I have to do this fit and layout rubbish manually here; I guess it is popupwindow screwing up as usual
|
|
|
|
if wx.Window.FindFocus() == self:
|
|
|
|
( my_width, my_height ) = self.GetSize()
|
|
|
|
#if len( self._dropdown_list ) > 0: self._dropdown_list.Show()
|
|
#else: self._dropdown_list.Hide()
|
|
|
|
self._dropdown_window.Fit()
|
|
|
|
self._dropdown_window.SetSize( ( my_width, -1 ) )
|
|
|
|
self._dropdown_window.Layout()
|
|
|
|
self._dropdown_window.SetPosition( self.ClientToScreenXY( -2, my_height - 2 ) )
|
|
|
|
self._dropdown_window.Show()
|
|
|
|
|
|
|
|
def _UpdateList( self ):
|
|
|
|
matches = self._GenerateMatches()
|
|
|
|
self._dropdown_list.SetTags( matches )
|
|
|
|
self._current_matches = matches
|
|
|
|
|
|
def EventFileButton( self, event ):
|
|
|
|
service_identifiers = wx.GetApp().Read( 'service_identifiers', ( HC.FILE_REPOSITORY, ) )
|
|
|
|
menu = wx.Menu()
|
|
|
|
if len( service_identifiers ) > 0: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_file_repository', CC.NULL_SERVICE_IDENTIFIER ), 'all known files' )
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_file_repository', CC.LOCAL_FILE_SERVICE_IDENTIFIER ), 'local files' )
|
|
|
|
for service_identifier in service_identifiers: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_file_repository', service_identifier ), service_identifier.GetName() )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
try:
|
|
|
|
( command, data ) = action
|
|
|
|
if command == 'change_file_repository':
|
|
|
|
service_identifier = data
|
|
|
|
self._file_service_identifier = service_identifier
|
|
|
|
if service_identifier == CC.NULL_SERVICE_IDENTIFIER: name = 'all known files'
|
|
else: name = service_identifier.GetName()
|
|
|
|
self._file_repo_button.SetLabel( name )
|
|
|
|
HC.pubsub.pub( 'change_file_repository', self._page_key, service_identifier )
|
|
|
|
elif command == 'change_tag_repository':
|
|
|
|
service_identifier = data
|
|
|
|
self._tag_service_identifier = service_identifier
|
|
|
|
if service_identifier == CC.NULL_SERVICE_IDENTIFIER: name = 'all known tags'
|
|
else: name = service_identifier.GetName()
|
|
|
|
self._tag_repo_button.SetLabel( name )
|
|
|
|
HC.pubsub.pub( 'change_tag_repository', self._page_key, service_identifier )
|
|
|
|
else:
|
|
|
|
event.Skip()
|
|
|
|
return # this is about select_up and select_down
|
|
|
|
|
|
self._first_letters = ''
|
|
self._current_namespace = ''
|
|
|
|
self._UpdateList()
|
|
|
|
except Exception as e:
|
|
|
|
wx.MessageBox( unicode( e ) )
|
|
wx.MessageBox( traceback.format_exc() )
|
|
|
|
|
|
|
|
|
|
def EventTagButton( self, event ):
|
|
|
|
service_identifiers = wx.GetApp().Read( 'service_identifiers', ( HC.TAG_REPOSITORY, ) )
|
|
|
|
menu = wx.Menu()
|
|
|
|
if len( service_identifiers ) > 0: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_tag_repository', CC.NULL_SERVICE_IDENTIFIER ), 'all known tags' )
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_tag_repository', CC.LOCAL_TAG_SERVICE_IDENTIFIER ), 'local tags' )
|
|
|
|
for service_identifier in service_identifiers: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_tag_repository', service_identifier ), service_identifier.GetName() )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
|
|
class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
|
|
|
|
def __init__( self, parent, page_key, file_service_identifier, tag_service_identifier, media_callable ):
|
|
|
|
AutoCompleteDropdownTags.__init__( self, parent, file_service_identifier, tag_service_identifier )
|
|
|
|
self._media_callable = media_callable
|
|
self._page_key = page_key
|
|
|
|
self._include_current = True
|
|
self._include_pending = True
|
|
|
|
self._include_current_tags = OnOffButton( self._dropdown_window, self._page_key, 'notify_include_current', on_label = 'include current tags', off_label = 'exclude current tags' )
|
|
self._include_current_tags.SetToolTipString( 'select whether to include current tags in the search' )
|
|
self._include_pending_tags = OnOffButton( self._dropdown_window, self._page_key, 'notify_include_pending', on_label = 'include pending tags', off_label = 'exclude pending tags' )
|
|
self._include_pending_tags.SetToolTipString( 'select whether to include pending tags in the search' )
|
|
|
|
self._synchronised = OnOffButton( self._dropdown_window, self._page_key, 'notify_search_immediately', on_label = 'searching immediately', off_label = 'waiting' )
|
|
self._synchronised.SetToolTipString( 'select whether to renew the search as soon as a new predicate is entered' )
|
|
|
|
button_hbox_1 = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
button_hbox_1.AddF( self._include_current_tags, FLAGS_EXPAND_BOTH_WAYS )
|
|
button_hbox_1.AddF( self._include_pending_tags, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
button_hbox_2 = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
button_hbox_2.AddF( self._file_repo_button, FLAGS_EXPAND_BOTH_WAYS )
|
|
button_hbox_2.AddF( self._tag_repo_button, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( button_hbox_1, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.AddF( self._synchronised, FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.AddF( button_hbox_2, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.AddF( self._dropdown_list, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._dropdown_window.SetSizer( vbox )
|
|
|
|
HC.pubsub.sub( self, 'SetSynchronisedWait', 'synchronised_wait_switch' )
|
|
|
|
HC.pubsub.sub( self, 'IncludeCurrent', 'notify_include_current' )
|
|
HC.pubsub.sub( self, 'IncludePending', 'notify_include_pending' )
|
|
|
|
|
|
def _BroadcastChoice( self, predicate ): HC.pubsub.pub( 'add_predicate', self._page_key, predicate )
|
|
|
|
def _GenerateMatches( self ):
|
|
|
|
num_first_letters = self._options[ 'num_autocomplete_chars' ]
|
|
|
|
raw_entry = self.GetValue()
|
|
|
|
if raw_entry.startswith( '-' ): search_text = raw_entry[1:]
|
|
else: search_text = raw_entry
|
|
|
|
search_text = HC.CleanTag( search_text )
|
|
|
|
if search_text == '':
|
|
|
|
self._first_letters = ''
|
|
self._current_namespace = ''
|
|
|
|
if self._file_service_identifier == CC.NULL_SERVICE_IDENTIFIER: s_i = self._tag_service_identifier
|
|
else: s_i = self._file_service_identifier
|
|
|
|
matches = wx.GetApp().Read( 'file_system_predicates', s_i )
|
|
|
|
else:
|
|
|
|
must_do_a_search = False
|
|
|
|
if ':' in search_text:
|
|
|
|
( namespace, half_complete_tag ) = search_text.split( ':' )
|
|
|
|
if namespace != self._current_namespace:
|
|
|
|
self._current_namespace = namespace # do a new search, no matter what half_complete tag is
|
|
|
|
must_do_a_search = True
|
|
|
|
|
|
else:
|
|
|
|
self._current_namespace = ''
|
|
|
|
half_complete_tag = search_text
|
|
|
|
|
|
if len( half_complete_tag ) >= num_first_letters:
|
|
|
|
if must_do_a_search or half_complete_tag[ : num_first_letters ] != self._first_letters:
|
|
|
|
self._first_letters = half_complete_tag[ : num_first_letters ]
|
|
|
|
media = self._media_callable()
|
|
|
|
if media is None: self._cached_results = wx.GetApp().Read( 'autocomplete_tags', file_service_identifier = self._file_service_identifier, tag_service_identifier = self._tag_service_identifier, half_complete_tag = search_text, include_current = self._include_current, include_pending = self._include_pending )
|
|
else:
|
|
|
|
all_tags = []
|
|
|
|
for m in media:
|
|
|
|
if m.IsCollection(): all_tags.extend( m.GetSingletonsTags() )
|
|
else: all_tags.append( m.GetTags() )
|
|
|
|
|
|
absolutely_all_tags = []
|
|
|
|
if self._tag_service_identifier == CC.NULL_SERVICE_IDENTIFIER:
|
|
|
|
if self._include_current: absolutely_all_tags += [ list( current ) for ( current, deleted, pending, petitioned ) in [ tags.GetUnionCDPP() for tags in all_tags ] ]
|
|
if self._include_pending: absolutely_all_tags += [ list( pending ) for ( current, deleted, pending, petitioned ) in [ tags.GetUnionCDPP() for tags in all_tags ] ]
|
|
|
|
else:
|
|
|
|
if self._include_current: absolutely_all_tags += [ list( current ) for ( current, deleted, pending, petitioned ) in [ tags.GetCDPP( self._tag_service_identifier ) for tags in all_tags ] ]
|
|
if self._include_pending: absolutely_all_tags += [ list( pending ) for ( current, deleted, pending, petitioned ) in [ tags.GetCDPP( self._tag_service_identifier ) for tags in all_tags ] ]
|
|
|
|
|
|
absolutely_all_tags_flat = [ tag for tags in absolutely_all_tags for tag in tags if HC.SearchEntryMatchesTag( half_complete_tag, tag ) ]
|
|
|
|
if self._current_namespace != '': absolutely_all_tags_flat = [ tag for tag in absolutely_all_tags_flat if tag.startswith( self._current_namespace + ':' ) ]
|
|
|
|
tags_to_count = collections.Counter( absolutely_all_tags_flat )
|
|
|
|
self._cached_results = CC.AutocompleteMatchesCounted( tags_to_count )
|
|
|
|
|
|
|
|
matches = self._cached_results.GetMatches( half_complete_tag )
|
|
|
|
if raw_entry.startswith( '-' ): matches = [ ( '-' + tag, count ) for ( tag, count ) in matches ]
|
|
|
|
else: matches = []
|
|
|
|
|
|
return matches
|
|
|
|
|
|
def IncludeCurrent( self, page_key, value ):
|
|
|
|
if page_key == self._page_key: self._include_current = value
|
|
|
|
self._first_letters = ''
|
|
self._current_namespace = ''
|
|
|
|
|
|
def IncludePending( self, page_key, value ):
|
|
|
|
if page_key == self._page_key: self._include_pending = value
|
|
|
|
self._first_letters = ''
|
|
self._current_namespace = ''
|
|
|
|
|
|
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, file_service_identifier, tag_service_identifier ):
|
|
|
|
self._chosen_tag_callable = chosen_tag_callable
|
|
|
|
self._page_key = None # this makes the parent's eventmenu pubsubs with page_key simpler!
|
|
|
|
self._options = wx.GetApp().Read( 'options' )
|
|
|
|
if self._options[ 'show_all_tags_in_autocomplete' ]: file_service_identifier = CC.NULL_SERVICE_IDENTIFIER
|
|
|
|
AutoCompleteDropdownTags.__init__( self, parent, file_service_identifier, tag_service_identifier )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox.AddF( self._file_repo_button, FLAGS_EXPAND_BOTH_WAYS )
|
|
hbox.AddF( self._tag_repo_button, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox.AddF( hbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.AddF( self._dropdown_list, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self._dropdown_window.SetSizer( vbox )
|
|
|
|
|
|
def _BroadcastChoice( self, predicate ): self._chosen_tag_callable( predicate )
|
|
|
|
def _GenerateMatches( self ):
|
|
|
|
num_first_letters = self._options[ 'num_autocomplete_chars' ]
|
|
|
|
raw_entry = self.GetValue()
|
|
|
|
search_text = HC.CleanTag( raw_entry )
|
|
|
|
if search_text == '':
|
|
|
|
self._first_letters = ''
|
|
self._current_namespace = ''
|
|
|
|
matches = []
|
|
|
|
else:
|
|
|
|
must_do_a_search = False
|
|
|
|
if ':' in search_text:
|
|
|
|
( namespace, half_complete_tag ) = search_text.split( ':' )
|
|
|
|
if namespace != self._current_namespace:
|
|
|
|
self._current_namespace = namespace # do a new search, no matter what half_complete tag is
|
|
|
|
must_do_a_search = True
|
|
|
|
|
|
else:
|
|
|
|
self._current_namespace = ''
|
|
|
|
half_complete_tag = search_text
|
|
|
|
|
|
# this bit obviously now needs an overhaul; we want to change to broader search domains automatically, based on what the user has selected
|
|
# (and hopefully show that in the buttons, temporarily)
|
|
|
|
if len( half_complete_tag ) >= num_first_letters:
|
|
|
|
if must_do_a_search or half_complete_tag[ : num_first_letters ] != self._first_letters:
|
|
|
|
self._first_letters = half_complete_tag[ : num_first_letters ]
|
|
|
|
self._cached_results = wx.GetApp().Read( 'autocomplete_tags', file_service_identifier = self._file_service_identifier, tag_service_identifier = self._tag_service_identifier, half_complete_tag = search_text )
|
|
|
|
|
|
matches = self._cached_results.GetMatches( half_complete_tag )
|
|
|
|
else: matches = []
|
|
|
|
try:
|
|
|
|
tags_in_order = [ tag for ( tag, count ) in matches ]
|
|
|
|
index = tags_in_order.index( search_text )
|
|
|
|
match = matches[ index ]
|
|
|
|
matches.remove( match )
|
|
|
|
matches.insert( 0, match )
|
|
|
|
except: matches.insert( 0, ( search_text, 0 ) )
|
|
|
|
|
|
return matches
|
|
|
|
|
|
class BufferedWindow( wx.Window ):
|
|
|
|
def __init__( self, *args, **kwargs ):
|
|
|
|
wx.Window.__init__( self, *args, **kwargs )
|
|
|
|
if 'size' in kwargs:
|
|
|
|
( x, y ) = kwargs[ 'size' ]
|
|
|
|
self._canvas_bmp = wx.EmptyBitmap( x, y, 24 )
|
|
|
|
else: self._canvas_bmp = wx.EmptyBitmap( 0, 0, 24 )
|
|
|
|
self.Bind( wx.EVT_PAINT, self.EventPaint )
|
|
self.Bind( wx.EVT_SIZE, self.EventResize )
|
|
|
|
|
|
|
|
def GetDC( self ): return wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
|
|
|
|
def EventPaint( self, event ): wx.BufferedPaintDC( self, self._canvas_bmp )
|
|
|
|
def EventResize( self, event ):
|
|
|
|
( my_width, my_height ) = self.GetClientSize()
|
|
|
|
( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize()
|
|
|
|
if my_width != current_bmp_width or my_height != current_bmp_height: self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 )
|
|
|
|
|
|
class BetterChoice( wx.Choice ):
|
|
|
|
def GetChoice( self ):
|
|
|
|
selection = self.GetSelection()
|
|
|
|
if selection != wx.NOT_FOUND: return self.GetClientData( selection )
|
|
else: raise Exception( 'choice not chosen' )
|
|
|
|
|
|
class CheckboxCollect( wx.combo.ComboCtrl ):
|
|
|
|
def __init__( self, parent, page_key = None, sort_by = None ):
|
|
|
|
wx.combo.ComboCtrl.__init__( self, parent, style = wx.CB_READONLY )
|
|
|
|
self._page_key = page_key
|
|
|
|
options = wx.GetApp().Read( 'options' )
|
|
|
|
if sort_by is None: sort_by = options[ 'sort_by' ]
|
|
|
|
all_namespaces = set()
|
|
|
|
for ( sort_by_type, namespaces ) in sort_by: all_namespaces.update( namespaces )
|
|
|
|
all_namespaces = list( all_namespaces )
|
|
all_namespaces.sort()
|
|
|
|
popup = CheckboxCollectDropdown( all_namespaces )
|
|
|
|
#self.UseAltPopupWindow( True )
|
|
|
|
self.SetPopupControl( popup )
|
|
|
|
|
|
def GetChoice( self ): return self._collect_by
|
|
|
|
def SetNamespaces( self, namespaces ):
|
|
|
|
namespaces = list( namespaces )
|
|
|
|
namespaces.sort()
|
|
|
|
if len( namespaces ) > 0:
|
|
|
|
self.SetValue( 'collect by ' + '-'.join( namespaces ) )
|
|
|
|
self._collect_by = namespaces
|
|
|
|
else:
|
|
|
|
self.SetValue( 'no collections' )
|
|
|
|
self._collect_by = None
|
|
|
|
|
|
HC.pubsub.pub( 'collect_media', self._page_key, self._collect_by )
|
|
|
|
|
|
class CheckboxCollectDropdown( wx.combo.ComboPopup ):
|
|
|
|
def __init__( self, namespaces ):
|
|
|
|
wx.combo.ComboPopup.__init__( self )
|
|
|
|
self._namespaces = namespaces
|
|
|
|
|
|
def Create( self, parent ):
|
|
|
|
self._control = CheckboxCollectDropdownCheckListBox( parent, self.GetCombo(), self._namespaces )
|
|
|
|
return True
|
|
|
|
|
|
def GetAdjustedSize( self, preferred_width, preferred_height, max_height ):
|
|
|
|
return( ( preferred_width, -1 ) )
|
|
|
|
|
|
def GetControl( self ): return self._control
|
|
|
|
class CheckboxCollectDropdownCheckListBox( wx.CheckListBox ):
|
|
|
|
def __init__( self, parent, special_parent, namespaces ):
|
|
|
|
wx.CheckListBox.__init__( self, parent, choices = namespaces )
|
|
|
|
self._special_parent = special_parent
|
|
|
|
options = wx.GetApp().Read( 'options' )
|
|
|
|
default = options[ 'default_collect' ] # need to reset this to a list of a set in options!
|
|
|
|
if default is not None: self.SetCheckedStrings( default )
|
|
|
|
self.Bind( wx.EVT_CHECKLISTBOX, self.EventChanged )
|
|
|
|
self.Bind( wx.EVT_LEFT_DOWN, self.EventLeftDown )
|
|
|
|
self.EventChanged( None )
|
|
|
|
|
|
# as inspired by http://trac.wxwidgets.org/attachment/ticket/14413/test_clb_workaround.py
|
|
# what a clusterfuck
|
|
|
|
def EventLeftDown( self, event ):
|
|
|
|
index = self.HitTest( event.GetPosition() )
|
|
|
|
if index != wx.NOT_FOUND:
|
|
|
|
self.Check( index, not self.IsChecked( index ) )
|
|
|
|
self.EventChanged( event )
|
|
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventChanged( self, event ):
|
|
|
|
namespaces = self.GetCheckedStrings()
|
|
|
|
self._special_parent.SetNamespaces( namespaces )
|
|
|
|
|
|
class ChoiceCollect( BetterChoice ):
|
|
|
|
def __init__( self, parent, page_key = None, sort_by = None ):
|
|
|
|
BetterChoice.__init__( self, parent )
|
|
|
|
self._page_key = page_key
|
|
|
|
options = wx.GetApp().Read( 'options' )
|
|
|
|
if sort_by is None: sort_by = options[ 'sort_by' ]
|
|
|
|
collect_choices = CC.GenerateCollectByChoices( sort_by )
|
|
|
|
for ( string, data ) in collect_choices: self.Append( string, data )
|
|
|
|
self.SetSelection( options[ 'default_collect' ] )
|
|
|
|
self.Bind( wx.EVT_CHOICE, self.EventChoice )
|
|
|
|
|
|
def EventChoice( self, event ):
|
|
|
|
if self._page_key is not None:
|
|
|
|
selection = self.GetSelection()
|
|
|
|
if selection != wx.NOT_FOUND:
|
|
|
|
collect_by = self.GetClientData( selection )
|
|
|
|
HC.pubsub.pub( 'collect_media', self._page_key, collect_by )
|
|
|
|
|
|
|
|
|
|
class ChoiceSort( BetterChoice ):
|
|
|
|
def __init__( self, parent, page_key = None, sort_by = None ):
|
|
|
|
BetterChoice.__init__( self, parent )
|
|
|
|
self._page_key = page_key
|
|
|
|
options = wx.GetApp().Read( 'options' )
|
|
|
|
if sort_by is None: sort_by = options[ 'sort_by' ]
|
|
|
|
sort_choices = CC.SORT_CHOICES + sort_by
|
|
|
|
ratings_service_identifiers = wx.GetApp().Read( 'service_identifiers', ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
|
|
|
|
for ratings_service_identifier in ratings_service_identifiers:
|
|
|
|
sort_choices.append( ( 'rating_descend', ratings_service_identifier ) )
|
|
sort_choices.append( ( 'rating_ascend', ratings_service_identifier ) )
|
|
|
|
|
|
for ( sort_by_type, sort_by_data ) in sort_choices:
|
|
|
|
if sort_by_type == 'system': string = CC.sort_string_lookup[ sort_by_data ]
|
|
elif sort_by_type == 'namespaces': string = '-'.join( sort_by_data )
|
|
elif sort_by_type == 'rating_descend': string = sort_by_data.GetName() + ' rating highest first'
|
|
elif sort_by_type == 'rating_ascend': string = sort_by_data.GetName() + ' rating lowest first'
|
|
|
|
self.Append( 'sort by ' + string, ( sort_by_type, sort_by_data ) )
|
|
|
|
|
|
try: self.SetSelection( options[ 'default_sort' ] )
|
|
except: pass
|
|
|
|
self.Bind( wx.EVT_CHOICE, self.EventChoice )
|
|
|
|
HC.pubsub.sub( self, 'ACollectHappened', 'collect_media' )
|
|
|
|
|
|
def _BroadcastSort( self ):
|
|
|
|
selection = self.GetSelection()
|
|
|
|
if selection != wx.NOT_FOUND:
|
|
|
|
sort_by = self.GetClientData( selection )
|
|
|
|
HC.pubsub.pub( 'sort_media', self._page_key, sort_by )
|
|
|
|
|
|
|
|
def ACollectHappened( self, page_key, collect_by ):
|
|
|
|
if page_key == self._page_key: self._BroadcastSort()
|
|
|
|
|
|
def EventChoice( self, event ):
|
|
|
|
if self._page_key is not None: self._BroadcastSort()
|
|
|
|
|
|
class FileDropTarget( wx.FileDropTarget ):
|
|
|
|
def __init__( self, callable ):
|
|
|
|
wx.FileDropTarget.__init__( self )
|
|
|
|
self._callable = callable
|
|
|
|
|
|
def OnDropFiles( self, x, y, paths ): wx.CallAfter( self._callable, paths )
|
|
|
|
class Frame( wx.Frame ):
|
|
|
|
def __init__( self, *args, **kwargs ):
|
|
|
|
wx.Frame.__init__( self, *args, **kwargs )
|
|
|
|
self._options = wx.GetApp().Read( 'options' )
|
|
|
|
#self.SetDoubleBuffered( True )
|
|
|
|
self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
|
|
|
|
|
|
class Gauge( wx.Gauge ):
|
|
|
|
def __init__( self, *args, **kwargs ):
|
|
|
|
wx.Gauge.__init__( self, *args, **kwargs )
|
|
|
|
self._actual_max = None
|
|
|
|
|
|
def SetRange( self, max ):
|
|
|
|
if max > 1000:
|
|
|
|
self._actual_max = max
|
|
wx.Gauge.SetRange( self, 1000 )
|
|
|
|
else:
|
|
|
|
self._actual_max = None
|
|
wx.Gauge.SetRange( self, max )
|
|
|
|
|
|
|
|
def SetValue( self, value ):
|
|
|
|
if self._actual_max is None: wx.Gauge.SetValue( self, value )
|
|
else: wx.Gauge.SetValue( self, min( int( 1000 * ( float( value ) / self._actual_max ) ), 1000 ) )
|
|
|
|
|
|
class ListBook( wx.Panel ):
|
|
|
|
def __init__( self, *args, **kwargs ):
|
|
|
|
wx.Panel.__init__( self, *args, **kwargs )
|
|
|
|
self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
|
|
|
|
self._list_box = wx.ListBox( self, style = wx.LB_SINGLE | wx.LB_SORT )
|
|
|
|
self._empty_panel = wx.Panel( self )
|
|
|
|
self._empty_panel.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
|
|
|
|
self._current_name = None
|
|
|
|
self._current_panel = self._empty_panel
|
|
|
|
self._panel_sizer = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
self._panel_sizer.AddF( self._empty_panel, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox.AddF( self._list_box, FLAGS_EXPAND_PERPENDICULAR )
|
|
hbox.AddF( self._panel_sizer, FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self._list_box.Bind( wx.EVT_LISTBOX, self.EventSelection )
|
|
|
|
self.SetSizer( hbox )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
|
|
def _RecalcListBoxWidth( self ): self.Layout()
|
|
|
|
def _Select( self, selection ):
|
|
|
|
with wx.FrozenWindow( self ):
|
|
|
|
if selection == wx.NOT_FOUND: self._current_name = None
|
|
else: self._current_name = self._list_box.GetString( selection )
|
|
|
|
self._current_panel.Hide()
|
|
|
|
self._list_box.SetSelection( selection )
|
|
|
|
if selection == wx.NOT_FOUND: self._current_panel = self._empty_panel
|
|
else:
|
|
|
|
panel_info = self._list_box.GetClientData( selection )
|
|
|
|
if type( panel_info ) == tuple:
|
|
|
|
( classname, args, kwargs ) = panel_info
|
|
|
|
page = classname( *args, **kwargs )
|
|
|
|
page.Hide()
|
|
|
|
self._panel_sizer.AddF( page, FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self._list_box.SetClientData( selection, page )
|
|
|
|
self._RecalcListBoxWidth()
|
|
|
|
|
|
self._current_panel = self._list_box.GetClientData( selection )
|
|
|
|
|
|
|
|
self._current_panel.Show()
|
|
|
|
self.Layout()
|
|
|
|
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, -1 )
|
|
|
|
self.ProcessEvent( event )
|
|
|
|
|
|
def AddPage( self, page, name, select = False ):
|
|
|
|
if type( page ) != tuple:
|
|
|
|
page.Hide()
|
|
|
|
self._panel_sizer.AddF( page, FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
|
|
self._list_box.Append( name, page )
|
|
|
|
self._RecalcListBoxWidth()
|
|
|
|
if self._list_box.GetCount() == 1: self._Select( 0 )
|
|
elif select: self._Select( self._list_box.FindString( name ) )
|
|
|
|
|
|
def DeleteAllPages( self ):
|
|
|
|
self._panel_sizer.Detach( self._empty_panel )
|
|
|
|
self._panel_sizer.Clear( deleteWindows = True )
|
|
|
|
self._panel_sizer.AddF( self._empty_panel, FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self._current_name = None
|
|
|
|
self._current_panel = self._empty_panel
|
|
|
|
self._list_box.Clear()
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
try:
|
|
|
|
( command, data ) = action
|
|
|
|
if command == 'select_down': self.SelectDown()
|
|
elif command == 'select_up': self.SelectUp()
|
|
else: event.Skip()
|
|
|
|
except Exception as e:
|
|
|
|
wx.MessageBox( unicode( e ) )
|
|
wx.MessageBox( traceback.format_exc() )
|
|
|
|
|
|
|
|
|
|
def EventSelection( self, event ):
|
|
|
|
if self._list_box.GetSelection() != self._list_box.FindString( self._current_name ):
|
|
|
|
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
|
|
|
|
self.GetEventHandler().ProcessEvent( event )
|
|
|
|
if event.IsAllowed(): self._Select( self._list_box.GetSelection() )
|
|
else: self._list_box.SetSelection( self._list_box.FindString( self._current_name ) )
|
|
|
|
|
|
|
|
def GetCurrentName( self ): return self._current_name
|
|
|
|
def GetCurrentPage( self ):
|
|
|
|
if self._current_panel == self._empty_panel: return None
|
|
else: return self._current_panel
|
|
|
|
|
|
def GetNameToPageDict( self ): return { self._list_box.GetString( i ) : self._list_box.GetClientData( i ) for i in range( self._list_box.GetCount() ) if type( self._list_box.GetClientData( i ) ) != tuple }
|
|
|
|
def NameExists( self, name, panel = None ): return self._list_box.FindString( name ) != wx.NOT_FOUND
|
|
|
|
def DeleteCurrentPage( self ):
|
|
|
|
selection = self._list_box.GetSelection()
|
|
|
|
if selection != wx.NOT_FOUND:
|
|
|
|
next_selection = selection + 1
|
|
previous_selection = selection - 1
|
|
|
|
if next_selection < self._list_box.GetCount(): self._Select( next_selection )
|
|
elif previous_selection >= 0: self._Select( previous_selection )
|
|
else: self._Select( wx.NOT_FOUND )
|
|
|
|
panel_info = self._list_box.GetClientData( selection )
|
|
|
|
if type( panel_info ) != tuple: self._panel_sizer.Remove( panel_info )
|
|
|
|
self._list_box.Delete( selection )
|
|
|
|
self._RecalcListBoxWidth()
|
|
|
|
|
|
|
|
def RenamePage( self, name, new_name ):
|
|
|
|
if self._list_box.FindString( new_name ) != wx.NOT_FOUND: raise Exception( 'That name is already in use!' )
|
|
|
|
if self._current_name == name: self._current_name = new_name
|
|
|
|
self._list_box.SetString( self._list_box.FindString( name ), new_name )
|
|
|
|
self._RecalcListBoxWidth()
|
|
|
|
|
|
def Select( self, name ):
|
|
|
|
selection = self._list_box.FindString( name )
|
|
|
|
if selection != wx.NOT_FOUND and selection != self._list_box.GetSelection():
|
|
|
|
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
|
|
|
|
self.GetEventHandler().ProcessEvent( event )
|
|
|
|
if event.IsAllowed(): self._Select( selection )
|
|
|
|
|
|
|
|
def SelectDown( self ):
|
|
|
|
current_selection = self._list_box.FindString( self._current_name )
|
|
|
|
if current_selection != wx.NOT_FOUND:
|
|
|
|
num_entries = self._list_box.GetCount()
|
|
|
|
if current_selection == num_entries - 1: selection = 0
|
|
else: selection = current_selection + 1
|
|
|
|
if selection != current_selection: self._Select( selection )
|
|
|
|
|
|
|
|
def SelectPage( self, page ):
|
|
|
|
for i in range( self._list_box.GetCount() ):
|
|
|
|
if self._list_box.GetClientData( i ) == page:
|
|
|
|
self._Select( i )
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def SelectUp( self ):
|
|
|
|
current_selection = self._list_box.FindString( self._current_name )
|
|
|
|
if current_selection != wx.NOT_FOUND:
|
|
|
|
num_entries = self._list_box.GetCount()
|
|
|
|
if current_selection == 0: selection = num_entries - 1
|
|
else: selection = current_selection - 1
|
|
|
|
if selection != current_selection: self._Select( selection )
|
|
|
|
|
|
|
|
class ListCtrlAutoWidth( wx.ListCtrl, ListCtrlAutoWidthMixin ):
|
|
|
|
def __init__( self, parent, height ):
|
|
|
|
wx.ListCtrl.__init__( self, parent, size=( -1, height ), style=wx.LC_REPORT )
|
|
ListCtrlAutoWidthMixin.__init__( self )
|
|
|
|
|
|
def GetAllSelected( self ):
|
|
|
|
indices = []
|
|
|
|
i = self.GetFirstSelected()
|
|
|
|
while i != -1:
|
|
|
|
indices.append( i )
|
|
|
|
i = self.GetNextSelected( i )
|
|
|
|
|
|
return indices
|
|
|
|
|
|
def RemoveAllSelected( self ):
|
|
|
|
indices = self.GetAllSelected()
|
|
|
|
indices.reverse() # so we don't screw with the indices of deletees below
|
|
|
|
for index in indices: self.DeleteItem( index )
|
|
|
|
|
|
class OnOffButton( wx.Button ):
|
|
|
|
def __init__( self, parent, page_key, topic, on_label, off_label = None, start_on = True ):
|
|
|
|
if start_on: label = on_label
|
|
else: label = off_label
|
|
|
|
wx.Button.__init__( self, parent, label = label )
|
|
|
|
self._page_key = page_key
|
|
self._topic = topic
|
|
self._on_label = on_label
|
|
|
|
if off_label is None: self._off_label = on_label
|
|
else: self._off_label = off_label
|
|
|
|
self._on = start_on
|
|
|
|
if self._on: self.SetForegroundColour( ( 0, 128, 0 ) )
|
|
else: self.SetForegroundColour( ( 128, 0, 0 ) )
|
|
|
|
self.Bind( wx.EVT_BUTTON, self.EventButton )
|
|
|
|
HC.pubsub.sub( self, 'HitButton', 'hit_on_off_button' )
|
|
|
|
|
|
def EventButton( self, event ):
|
|
|
|
if self._on:
|
|
|
|
self._on = False
|
|
|
|
self.SetLabel( self._off_label )
|
|
|
|
self.SetForegroundColour( ( 128, 0, 0 ) )
|
|
|
|
HC.pubsub.pub( self._topic, self._page_key, False )
|
|
|
|
else:
|
|
|
|
self._on = True
|
|
|
|
self.SetLabel( self._on_label )
|
|
|
|
self.SetForegroundColour( ( 0, 128, 0 ) )
|
|
|
|
HC.pubsub.pub( self._topic, self._page_key, True )
|
|
|
|
|
|
|
|
def IsOn( self ): return self._on
|
|
|
|
class NoneableSpinCtrl( wx.Panel ):
|
|
|
|
def __init__( self, parent, message, value, none_phrase = 'no limit', max = 1000000, multiplier = 1, num_dimensions = 1 ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self._num_dimensions = num_dimensions
|
|
self._multiplier = multiplier
|
|
|
|
self._checkbox = wx.CheckBox( self, label = none_phrase )
|
|
self._checkbox.Bind( wx.EVT_CHECKBOX, self.EventCheckBox )
|
|
|
|
if value is None:
|
|
|
|
self._one = wx.SpinCtrl( self, initial = 0, max = max, size = ( 80, -1 ) )
|
|
self._one.Disable()
|
|
|
|
if num_dimensions == 2:
|
|
|
|
self._two = wx.SpinCtrl( self, initial = 0, max = max, size = ( 80, -1 ) )
|
|
self._two.Disable()
|
|
|
|
|
|
self._checkbox.SetValue( True )
|
|
|
|
else:
|
|
|
|
if num_dimensions == 2:
|
|
|
|
( value, value_2 ) = value
|
|
|
|
self._two = wx.SpinCtrl( self, max = max, size = ( 80, -1 ) )
|
|
self._two.SetValue( value_2 / multiplier )
|
|
|
|
|
|
self._one = wx.SpinCtrl( self, max = max, size = ( 80, -1 ) )
|
|
self._one.SetValue( value / multiplier )
|
|
|
|
self._checkbox.SetValue( False )
|
|
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
hbox.AddF( wx.StaticText( self, label=message + ': ' ), FLAGS_MIXED )
|
|
hbox.AddF( self._one, FLAGS_MIXED )
|
|
|
|
if self._num_dimensions == 2:
|
|
|
|
hbox.AddF( wx.StaticText( self, label = 'x' ), FLAGS_MIXED )
|
|
hbox.AddF( self._two, FLAGS_MIXED )
|
|
|
|
|
|
hbox.AddF( self._checkbox, FLAGS_MIXED )
|
|
|
|
self.SetSizer( hbox )
|
|
|
|
|
|
def EventCheckBox( self, event ):
|
|
|
|
if self._checkbox.GetValue():
|
|
|
|
self._one.Disable()
|
|
if self._num_dimensions == 2: self._two.Disable()
|
|
|
|
else:
|
|
|
|
self._one.Enable()
|
|
if self._num_dimensions == 2: self._two.Enable()
|
|
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
if self._checkbox.GetValue(): return None
|
|
else:
|
|
|
|
if self._num_dimensions == 2: return ( self._one.GetValue() * self._multiplier, self._two.GetValue() * self._multiplier )
|
|
else: return self._one.GetValue() * self._multiplier
|
|
|
|
|
|
|
|
def SetValue( self, value ):
|
|
|
|
if value is None:
|
|
|
|
self._checkbox.SetValue( True )
|
|
|
|
self._one.Disable()
|
|
if self._num_dimensions == 2: self._two.Disable()
|
|
|
|
else:
|
|
|
|
self._checkbox.SetValue( False )
|
|
|
|
self._one.Enable()
|
|
if self._num_dimensions == 2: self._two.Enable()
|
|
|
|
if self._num_dimensions == 2:
|
|
|
|
( value, y ) = value
|
|
|
|
self._two.SetValue( y / self._multiplier )
|
|
|
|
|
|
self._one.SetValue( value / self._multiplier )
|
|
|
|
|
|
|
|
class ListBox( wx.ScrolledWindow ):
|
|
|
|
def __init__( self, parent, min_height = 250 ):
|
|
|
|
wx.ScrolledWindow.__init__( self, parent, style = wx.VSCROLL | wx.BORDER_DOUBLE )
|
|
|
|
self._ordered_strings = []
|
|
self._strings_to_terms = {}
|
|
|
|
self._options = wx.GetApp().Read( 'options' )
|
|
|
|
self._canvas_bmp = wx.EmptyBitmap( 0, 0, 24 )
|
|
|
|
self._current_selected_index = None
|
|
|
|
dc = self._GetScrolledDC()
|
|
|
|
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
|
|
|
|
( text_x, self._text_y ) = dc.GetTextExtent( 'abcdefghijklmnopqrstuvwxyz' )
|
|
|
|
self._num_rows_per_page = 0
|
|
|
|
self.SetScrollRate( 0, self._text_y )
|
|
|
|
self.SetMinSize( ( 50, min_height ) )
|
|
|
|
self.Bind( wx.EVT_PAINT, self.EventPaint )
|
|
self.Bind( wx.EVT_SIZE, self.EventResize )
|
|
|
|
self.Bind( wx.EVT_LEFT_DOWN, self.EventMouseSelect )
|
|
self.Bind( wx.EVT_LEFT_DCLICK, self.EventDClick )
|
|
|
|
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
|
|
|
|
|
|
def __len__( self ): return len( self._ordered_strings )
|
|
|
|
def _Activate( self, tag ): pass
|
|
|
|
def _DrawTexts( self ):
|
|
|
|
( my_width, my_height ) = self.GetClientSize()
|
|
|
|
dc = self._GetScrolledDC()
|
|
|
|
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
|
|
|
|
i = 0
|
|
|
|
dc.SetBackground( wx.Brush( wx.Colour( 255, 255, 255 ) ) )
|
|
|
|
dc.Clear()
|
|
|
|
for ( i, text ) in enumerate( self._ordered_strings ):
|
|
|
|
( r, g, b ) = self._GetTextColour( text )
|
|
|
|
text_colour = wx.Colour( r, g, b )
|
|
|
|
if self._current_selected_index is not None and i == self._current_selected_index:
|
|
|
|
dc.SetBrush( wx.Brush( text_colour ) )
|
|
|
|
dc.SetPen( wx.TRANSPARENT_PEN )
|
|
|
|
dc.DrawRectangle( 0, i * self._text_y, my_width, self._text_y )
|
|
|
|
text_colour = wx.WHITE
|
|
|
|
|
|
dc.SetTextForeground( text_colour )
|
|
|
|
( x, y ) = ( 3, i * self._text_y )
|
|
|
|
dc.DrawText( text, x, y )
|
|
|
|
|
|
|
|
def _GetIndexUnderMouse( self, mouse_event ):
|
|
|
|
( xUnit, yUnit ) = self.GetScrollPixelsPerUnit()
|
|
|
|
( x_scroll, y_scroll ) = self.GetViewStart()
|
|
|
|
y_offset = y_scroll * yUnit
|
|
|
|
y = mouse_event.GetY() + y_offset
|
|
|
|
row_index = ( y / self._text_y )
|
|
|
|
if row_index >= len( self._ordered_strings ): return None
|
|
|
|
return row_index
|
|
|
|
|
|
def _GetScrolledDC( self ):
|
|
|
|
cdc = wx.ClientDC( self )
|
|
|
|
self.DoPrepareDC( cdc ) # because this is a scrolled window
|
|
|
|
return wx.BufferedDC( cdc, self._canvas_bmp )
|
|
|
|
|
|
def _GetTextColour( self, text ): return ( 0, 111, 250 )
|
|
|
|
def _Select( self, index ):
|
|
|
|
if index is not None:
|
|
|
|
if index == -1 or index > len( self._ordered_strings ): index = len( self._ordered_strings ) - 1
|
|
elif index == len( self._ordered_strings ) or index < -1: index = 0
|
|
|
|
|
|
self._current_selected_index = index
|
|
|
|
self._DrawTexts()
|
|
|
|
if self._current_selected_index is not None:
|
|
|
|
# scroll to index, if needed
|
|
|
|
y = self._text_y * self._current_selected_index
|
|
|
|
( start_x, start_y ) = self.GetViewStart()
|
|
|
|
( x_unit, y_unit ) = self.GetScrollPixelsPerUnit()
|
|
|
|
( width, height ) = self.GetClientSize()
|
|
|
|
if y < start_y * y_unit:
|
|
|
|
y_to_scroll_to = y / y_unit
|
|
|
|
self.Scroll( -1, y_to_scroll_to )
|
|
|
|
wx.PostEvent( self, wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE ) )
|
|
|
|
elif y > ( start_y * y_unit ) + height:
|
|
|
|
y_to_scroll_to = ( y - height ) / y_unit
|
|
|
|
self.Scroll( -1, y_to_scroll_to + 3 )
|
|
|
|
wx.PostEvent( self, wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE ) )
|
|
|
|
|
|
|
|
|
|
def _TextsHaveChanged( self ):
|
|
|
|
self._current_selected_index = None
|
|
|
|
total_height = self._text_y * len( self._ordered_strings )
|
|
|
|
( my_x, my_y ) = self._canvas_bmp.GetSize()
|
|
|
|
if my_y != total_height: wx.PostEvent( self, wx.SizeEvent() )
|
|
else: self._DrawTexts()
|
|
|
|
|
|
def EventDClick( self, event ):
|
|
|
|
index = self._GetIndexUnderMouse( event )
|
|
|
|
if index is not None and index == self._current_selected_index: self._Activate( self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ] )
|
|
|
|
|
|
def EventKeyDown( self, event ):
|
|
|
|
key_code = event.GetKeyCode()
|
|
|
|
if self._current_selected_index is not None:
|
|
|
|
if key_code in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ): self._Activate( self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ] )
|
|
elif key_code in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ): self._Select( self._current_selected_index - 1 )
|
|
elif key_code in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ): self._Select( self._current_selected_index + 1 )
|
|
elif key_code == wx.WXK_PAGEUP: self._Select( self._current_selected_index - self._num_rows_per_page )
|
|
elif key_code == wx.WXK_PAGEDOWN: self._Select( self._current_selected_index + self._num_rows_per_page )
|
|
else: event.Skip()
|
|
|
|
else: event.Skip()
|
|
|
|
|
|
def EventMouseSelect( self, event ):
|
|
|
|
index = self._GetIndexUnderMouse( event )
|
|
|
|
self._Select( index )
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventPaint( self, event ): wx.BufferedPaintDC( self, self._canvas_bmp, wx.BUFFER_VIRTUAL_AREA )
|
|
|
|
def EventResize( self, event ):
|
|
|
|
( client_x, client_y ) = self.GetClientSize()
|
|
|
|
( my_x, my_y ) = self._canvas_bmp.GetSize()
|
|
|
|
self._num_rows_per_page = client_y / self._text_y
|
|
|
|
total_height = self._text_y * len( self._ordered_strings )
|
|
|
|
if my_x != client_x or my_y != total_height:
|
|
|
|
new_y = max( client_y, total_height )
|
|
|
|
self.SetVirtualSize( ( client_x, new_y ) )
|
|
|
|
self._canvas_bmp = wx.EmptyBitmap( client_x, new_y, 24 )
|
|
|
|
self._DrawTexts()
|
|
|
|
|
|
|
|
def SetTexts( self, ordered_strings ):
|
|
|
|
if ordered_strings != self._ordered_strings:
|
|
|
|
self._ordered_strings = ordered_strings
|
|
self._strings_to_terms = { s : s for s in ordered_strings }
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
if len( ordered_strings ) > 0: self._Select( 0 )
|
|
|
|
|
|
|
|
class ListBoxMessages( ListBox ):
|
|
|
|
def _GetTextColour( self, predicate_string ):
|
|
|
|
if predicate_string.startswith( 'system:' ): ( r, g, b ) = ( 153, 101, 21 )
|
|
else: ( r, g, b ) = ( 0, 111, 250 )
|
|
|
|
return ( r, g, b )
|
|
|
|
|
|
class ListBoxMessagesActiveOnly( ListBoxMessages ):
|
|
|
|
def __init__( self, parent, callable ):
|
|
|
|
ListBoxMessages.__init__( self, parent )
|
|
|
|
self._callable = callable
|
|
|
|
self._matches = {}
|
|
|
|
|
|
def _Activate( self, tag ): self._callable( tag )
|
|
|
|
def SetTerms( self, matches ):
|
|
|
|
if matches != self._matches:
|
|
|
|
self._matches = matches
|
|
|
|
self._ordered_strings = []
|
|
self._strings_to_terms = {}
|
|
|
|
for ( term, count ) in matches:
|
|
|
|
if count is None: term_string = term
|
|
else: term_string = term + ' (' + HC.ConvertIntToPrettyString( count ) + ')'
|
|
|
|
self._ordered_strings.append( term_string )
|
|
self._strings_to_terms[ term_string ] = term
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
if len( matches ) > 0: self._Select( 0 )
|
|
|
|
|
|
|
|
class ListBoxMessagesPredicates( ListBoxMessages ):
|
|
|
|
def __init__( self, parent, page_key, initial_predicates = [] ):
|
|
|
|
ListBoxMessages.__init__( self, parent )
|
|
|
|
self._page_key = page_key
|
|
|
|
if len( initial_predicates ) > 0:
|
|
|
|
for predicate in initial_predicates:
|
|
|
|
self._ordered_strings.append( predicate )
|
|
self._strings_to_terms[ predicate ] = predicate
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
|
|
def _Activate( self, term ): HC.pubsub.pub( 'remove_predicate', self._page_key, term )
|
|
|
|
def ActivatePredicate( self, term ):
|
|
|
|
if term in self._ordered_strings:
|
|
|
|
self._ordered_strings.remove( term )
|
|
del self._strings_to_terms[ term ]
|
|
|
|
else:
|
|
|
|
if term == 'system:inbox' and 'system:archive' in self._ordered_strings: self._ordered_strings.remove( 'system:archive' )
|
|
elif term == 'system:archive' and 'system:inbox' in self._ordered_strings: self._ordered_strings.remove( 'system:inbox' )
|
|
|
|
self._ordered_strings.append( term )
|
|
self._strings_to_terms[ term ] = term
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def AddPredicate( self, predicate ):
|
|
|
|
self._ordered_strings.append( predicate )
|
|
self._strings_to_terms[ predicate ] = predicate
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def GetPredicates( self ): return self._ordered_strings
|
|
|
|
def HasPredicate( self, predicate ): return predicate in self._ordered_strings
|
|
|
|
def RemovePredicate( self, predicate ):
|
|
|
|
self._ordered_strings.remove( predicate )
|
|
del self._strings_to_terms[ predicate ]
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
|
|
|
|
def __init__( self, parent, height, columns ):
|
|
|
|
num_columns = len( columns )
|
|
|
|
wx.ListCtrl.__init__( self, parent, size=( -1, height ), style=wx.LC_REPORT )
|
|
ListCtrlAutoWidthMixin.__init__( self )
|
|
ColumnSorterMixin.__init__( self, num_columns )
|
|
|
|
self.GetTopLevelParent().SetDoubleBuffered( False ) # windows double buffer makes listctrls refresh and bug out
|
|
|
|
self.itemDataMap = {}
|
|
self._next_data_index = 0
|
|
|
|
resize_column = 1
|
|
|
|
for ( i, ( name, width ) ) in enumerate( columns ):
|
|
|
|
self.InsertColumn( i, name, width = width )
|
|
|
|
if width == -1: resize_column = i + 1
|
|
|
|
|
|
self.setResizeColumn( resize_column )
|
|
|
|
|
|
def Append( self, display_tuple, data_tuple ):
|
|
|
|
index = wx.ListCtrl.Append( self, display_tuple )
|
|
|
|
self.SetItemData( index, self._next_data_index )
|
|
|
|
self.itemDataMap[ self._next_data_index ] = list( data_tuple )
|
|
|
|
self._next_data_index += 1
|
|
|
|
|
|
def GetAllSelected( self ):
|
|
|
|
indices = []
|
|
|
|
i = self.GetFirstSelected()
|
|
|
|
while i != -1:
|
|
|
|
indices.append( i )
|
|
|
|
i = self.GetNextSelected( i )
|
|
|
|
|
|
return indices
|
|
|
|
|
|
def GetClientData( self, index = None ):
|
|
|
|
if index is None:
|
|
|
|
data_indicies = [ self.GetItemData( index ) for index in range( self.GetItemCount() ) ]
|
|
|
|
datas = [ self.itemDataMap[ data_index ] for data_index in data_indicies ]
|
|
|
|
return datas
|
|
|
|
else:
|
|
|
|
data_index = self.GetItemData( index )
|
|
|
|
return self.itemDataMap[ data_index ]
|
|
|
|
|
|
|
|
def GetListCtrl( self ): return self
|
|
|
|
def RemoveAllSelected( self ):
|
|
|
|
indices = self.GetAllSelected()
|
|
|
|
indices.reverse() # so we don't screw with the indices of deletees below
|
|
|
|
for index in indices: self.DeleteItem( index )
|
|
|
|
|
|
def UpdateValue( self, index, column, display_value, data_value ):
|
|
|
|
self.SetStringItem( index, column, display_value )
|
|
|
|
data_index = self.GetItemData( index )
|
|
|
|
self.itemDataMap[ data_index ][ column ] = data_value
|
|
|
|
|
|
def UpdateRow( self, index, display_tuple, data_tuple ):
|
|
|
|
column = 0
|
|
|
|
for value in display_tuple:
|
|
|
|
self.SetStringItem( index, column, value )
|
|
|
|
column += 1
|
|
|
|
|
|
data_index = self.GetItemData( index )
|
|
|
|
self.itemDataMap[ data_index ] = data_tuple
|
|
|
|
|
|
class Shortcut( wx.TextCtrl ):
|
|
|
|
def __init__( self, parent, modifier = wx.ACCEL_NORMAL, key = wx.WXK_F7 ):
|
|
|
|
self._modifier = modifier
|
|
self._key = key
|
|
|
|
wx.TextCtrl.__init__( self, parent, style = wx.TE_PROCESS_ENTER )
|
|
|
|
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
|
|
|
|
self._SetShortcutString()
|
|
|
|
|
|
def _SetShortcutString( self ):
|
|
|
|
display_string = ''
|
|
|
|
if self._modifier == wx.ACCEL_ALT: display_string += 'alt + '
|
|
elif self._modifier == wx.ACCEL_CTRL: display_string += 'ctrl + '
|
|
elif self._modifier == wx.ACCEL_SHIFT: display_string += 'shift + '
|
|
|
|
if self._key in range( 65, 91 ): display_string += chr( self._key + 32 ) # + 32 for converting ascii A -> a
|
|
elif self._key in range( 97, 123 ): display_string += chr( self._key )
|
|
else: display_string += HC.wxk_code_string_lookup[ self._key ]
|
|
|
|
wx.TextCtrl.SetValue( self, display_string )
|
|
|
|
|
|
def EventKeyDown( self, event ):
|
|
|
|
if event.KeyCode in range( 65, 91 ) or event.KeyCode in HC.wxk_code_string_lookup.keys():
|
|
|
|
modifier = wx.ACCEL_NORMAL
|
|
|
|
if event.AltDown(): modifier = wx.ACCEL_ALT
|
|
elif event.ControlDown(): modifier = wx.ACCEL_CTRL
|
|
elif event.ShiftDown(): modifier = wx.ACCEL_SHIFT
|
|
|
|
( self._modifier, self._key ) = HC.GetShortcutFromEvent( event )
|
|
|
|
self._SetShortcutString()
|
|
|
|
|
|
def GetValue( self ): return ( self._modifier, self._key )
|
|
|
|
def SetValue( self, modifier, key ):
|
|
|
|
( self._modifier, self._key ) = ( modifier, key )
|
|
|
|
self._SetShortcutString()
|
|
|
|
|
|
class StaticBox( wx.Panel ):
|
|
|
|
def __init__( self, parent, title ):
|
|
|
|
wx.Panel.__init__( self, parent, style = wx.BORDER_DOUBLE )
|
|
|
|
self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
|
|
|
|
self._sizer = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
normal_font = wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT )
|
|
|
|
normal_font_size = normal_font.GetPointSize()
|
|
normal_font_family = normal_font.GetFamily()
|
|
|
|
title_font = wx.Font( int( normal_font_size ), normal_font_family, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD )
|
|
|
|
title_text = wx.StaticText( self, label = title, style = wx.ALIGN_CENTER )
|
|
title_text.SetFont( title_font )
|
|
|
|
self._sizer.AddF( title_text, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.SetSizer( self._sizer )
|
|
|
|
|
|
def AddF( self, widget, flags ): self._sizer.AddF( widget, flags )
|
|
|
|
class AdvancedOptions( StaticBox ):
|
|
|
|
def __init__( self, parent, title ):
|
|
|
|
StaticBox.__init__( self, parent, title )
|
|
|
|
self._collapsible_panel = wx.CollapsiblePane( self, label = 'expand' )
|
|
|
|
my_panel = self._collapsible_panel.GetPane()
|
|
|
|
self._InitPanel( my_panel )
|
|
|
|
self.AddF( self._collapsible_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.Bind( wx.EVT_COLLAPSIBLEPANE_CHANGED, self.EventChanged )
|
|
|
|
|
|
def _InitPanel( self, panel ): pass
|
|
|
|
def EventChanged( self, event ):
|
|
|
|
self.GetParent().Layout() # make this vertical only?
|
|
|
|
if self._collapsible_panel.IsExpanded(): label = 'collapse'
|
|
else: label = 'expand'
|
|
|
|
self._collapsible_panel.SetLabel( label )
|
|
|
|
event.Skip()
|
|
|
|
|
|
class AdvancedHentaiFoundryOptions( AdvancedOptions ):
|
|
|
|
def __init__( self, parent ): AdvancedOptions.__init__( self, parent, 'advanced hentai foundry options' )
|
|
|
|
def _InitPanel( self, panel ):
|
|
|
|
def offensive_choice():
|
|
|
|
c = wx.Choice( panel )
|
|
|
|
c.Append( 'none', 0 )
|
|
c.Append( 'mild', 1 )
|
|
c.Append( 'moderate', 2 )
|
|
c.Append( 'strong', 3 )
|
|
|
|
c.SetSelection( 3 )
|
|
|
|
return c
|
|
|
|
|
|
self._rating_nudity = offensive_choice()
|
|
self._rating_violence = offensive_choice()
|
|
self._rating_profanity = offensive_choice()
|
|
self._rating_racism = offensive_choice()
|
|
self._rating_sex = offensive_choice()
|
|
self._rating_spoilers = offensive_choice()
|
|
|
|
self._rating_yaoi = wx.CheckBox( panel )
|
|
self._rating_yuri = wx.CheckBox( panel )
|
|
self._rating_loli = wx.CheckBox( panel )
|
|
self._rating_shota = wx.CheckBox( panel )
|
|
self._rating_teen = wx.CheckBox( panel )
|
|
self._rating_guro = wx.CheckBox( panel )
|
|
self._rating_furry = wx.CheckBox( panel )
|
|
self._rating_beast = wx.CheckBox( panel )
|
|
self._rating_male = wx.CheckBox( panel )
|
|
self._rating_female = wx.CheckBox( panel )
|
|
self._rating_futa = wx.CheckBox( panel )
|
|
self._rating_other = wx.CheckBox( panel )
|
|
|
|
self._rating_yaoi.SetValue( True )
|
|
self._rating_yuri.SetValue( True )
|
|
self._rating_loli.SetValue( True )
|
|
self._rating_shota.SetValue( True )
|
|
self._rating_teen.SetValue( True )
|
|
self._rating_guro.SetValue( True )
|
|
self._rating_furry.SetValue( True )
|
|
self._rating_beast.SetValue( True )
|
|
self._rating_male.SetValue( True )
|
|
self._rating_female.SetValue( True )
|
|
self._rating_futa.SetValue( True )
|
|
self._rating_other.SetValue( True )
|
|
|
|
self._filter_order = wx.Choice( panel )
|
|
|
|
self._filter_order.Append( 'newest first', 'date_new' )
|
|
self._filter_order.Append( 'oldest first', 'date_old' )
|
|
self._filter_order.Append( 'most views first', 'views most' ) # no underscore
|
|
self._filter_order.Append( 'highest rating first', 'rating highest' ) # no underscore
|
|
self._filter_order.Append( 'most favourites first', 'faves most' ) # no underscore
|
|
self._filter_order.Append( 'most popular first', 'popularity most' ) # no underscore
|
|
|
|
self._filter_order.SetSelection( 0 )
|
|
|
|
gridbox = wx.FlexGridSizer( 0, 2 )
|
|
|
|
gridbox.AddGrowableCol( 1, 1 )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'nudity' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_nudity, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'violence' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_violence, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'profanity' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_profanity, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'racism' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_racism, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'sex' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_sex, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'spoilers' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_spoilers, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'yaoi' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_yaoi, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'yuri' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_yuri, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'loli' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_loli, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'shota' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_shota, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'teen' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_teen, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'guro' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_guro, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'furry' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_furry, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'beast' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_beast, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'male' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_male, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'female' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_female, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'futa' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_futa, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'other' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._rating_other, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
gridbox.AddF( wx.StaticText( panel, label = 'order' ), FLAGS_MIXED )
|
|
gridbox.AddF( self._filter_order, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
panel.SetSizer( gridbox )
|
|
|
|
|
|
def GetInfo( self ):
|
|
|
|
info = {}
|
|
|
|
info[ 'rating_nudity' ] = self._rating_nudity.GetClientData( self._rating_nudity.GetSelection() )
|
|
info[ 'rating_violence' ] = self._rating_violence.GetClientData( self._rating_violence.GetSelection() )
|
|
info[ 'rating_profanity' ] = self._rating_profanity.GetClientData( self._rating_profanity.GetSelection() )
|
|
info[ 'rating_racism' ] = self._rating_racism.GetClientData( self._rating_racism.GetSelection() )
|
|
info[ 'rating_sex' ] = self._rating_sex.GetClientData( self._rating_sex.GetSelection() )
|
|
info[ 'rating_spoilers' ] = self._rating_spoilers.GetClientData( self._rating_spoilers.GetSelection() )
|
|
|
|
info[ 'rating_yaoi' ] = int( self._rating_yaoi.GetValue() )
|
|
info[ 'rating_yuri' ] = int( self._rating_yuri.GetValue() )
|
|
info[ 'rating_loli' ] = int( self._rating_loli.GetValue() )
|
|
info[ 'rating_shota' ] = int( self._rating_shota.GetValue() )
|
|
info[ 'rating_teen' ] = int( self._rating_teen.GetValue() )
|
|
info[ 'rating_guro' ] = int( self._rating_guro.GetValue() )
|
|
info[ 'rating_furry' ] = int( self._rating_furry.GetValue() )
|
|
info[ 'rating_beast' ] = int( self._rating_beast.GetValue() )
|
|
info[ 'rating_male' ] = int( self._rating_male.GetValue() )
|
|
info[ 'rating_female' ] = int( self._rating_female.GetValue() )
|
|
info[ 'rating_futa' ] = int( self._rating_futa.GetValue() )
|
|
info[ 'rating_other' ] = int( self._rating_other.GetValue() )
|
|
|
|
info[ 'filter_media' ] = 'A'
|
|
info[ 'filter_order' ] = self._filter_order.GetClientData( self._filter_order.GetSelection() )
|
|
info[ 'filter_type' ] = 0
|
|
|
|
return info
|
|
|
|
|
|
class AdvancedImportOptions( AdvancedOptions ):
|
|
|
|
def __init__( self, parent ): AdvancedOptions.__init__( self, parent, 'advanced import options' )
|
|
|
|
def _InitPanel( self, panel ):
|
|
|
|
options = wx.GetApp().Read( 'options' )
|
|
|
|
self._auto_archive = wx.CheckBox( panel )
|
|
self._auto_archive.SetValue( False )
|
|
|
|
self._exclude_deleted = wx.CheckBox( panel )
|
|
self._exclude_deleted.SetValue( options[ 'exclude_deleted_files' ] )
|
|
|
|
self._min_size = NoneableSpinCtrl( panel, 'minimum size (KB): ', 5120, multiplier = 1024 )
|
|
self._min_size.SetValue( None )
|
|
|
|
self._min_resolution = NoneableSpinCtrl( panel, 'minimum resolution: ', ( 50, 50 ), num_dimensions = 2 )
|
|
self._min_resolution.SetValue( None )
|
|
|
|
hbox1 = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox1.AddF( self._auto_archive, FLAGS_MIXED )
|
|
hbox1.AddF( wx.StaticText( panel, label = ' archive all imports' ), FLAGS_MIXED )
|
|
|
|
hbox2 = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox2.AddF( self._exclude_deleted, FLAGS_MIXED )
|
|
hbox2.AddF( wx.StaticText( panel, label = ' exclude already deleted files' ), FLAGS_MIXED )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( hbox1, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.AddF( hbox2, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.AddF( self._min_size, FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.AddF( self._min_resolution, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
panel.SetSizer( vbox )
|
|
|
|
|
|
def GetInfo( self ):
|
|
|
|
info = {}
|
|
|
|
if self._auto_archive.GetValue(): info[ 'auto_archive' ] = True
|
|
|
|
if self._exclude_deleted.GetValue(): info[ 'exclude_deleted_files' ] = True
|
|
|
|
min_size = self._min_size.GetValue()
|
|
|
|
if min_size is not None: info[ 'min_size' ] = min_size
|
|
|
|
min_resolution = self._min_resolution.GetValue()
|
|
|
|
if min_resolution is not None: info[ 'min_resolution' ] = min_resolution
|
|
|
|
return info
|
|
|
|
|
|
class AdvancedTagOptions( AdvancedOptions ):
|
|
|
|
def __init__( self, parent, info_string, namespaces = [] ):
|
|
|
|
self._info_string = info_string
|
|
self._namespaces = namespaces
|
|
|
|
self._checkboxes_to_service_identifiers = {}
|
|
self._service_identifiers_to_namespaces = {}
|
|
|
|
AdvancedOptions.__init__( self, parent, 'advanced tag options' )
|
|
|
|
|
|
def _InitPanel( self, panel ):
|
|
|
|
service_identifiers = wx.GetApp().Read( 'service_identifiers', ( HC.TAG_REPOSITORY, HC.LOCAL_TAG ) )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
if len( service_identifiers ) > 0:
|
|
|
|
for service_identifier in service_identifiers:
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
checkbox = wx.CheckBox( panel )
|
|
checkbox.Bind( wx.EVT_CHECKBOX, self.EventChecked )
|
|
|
|
self._checkboxes_to_service_identifiers[ checkbox ] = service_identifier
|
|
|
|
hbox.AddF( wx.StaticText( panel, label = service_identifier.GetName() ), FLAGS_MIXED )
|
|
hbox.AddF( checkbox, FLAGS_MIXED )
|
|
|
|
if len( self._namespaces ) > 0:
|
|
|
|
namespace_vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
self._service_identifiers_to_namespaces[ service_identifier ] = []
|
|
|
|
gridbox = wx.FlexGridSizer( 0, 2 )
|
|
|
|
gridbox.AddGrowableCol( 1, 1 )
|
|
|
|
for namespace in self._namespaces:
|
|
|
|
if namespace == '': text = wx.StaticText( panel, label = 'no namespace' )
|
|
else: text = wx.StaticText( panel, label = namespace )
|
|
|
|
namespace_checkbox = wx.CheckBox( panel )
|
|
namespace_checkbox.SetValue( True )
|
|
namespace_checkbox.Bind( wx.EVT_CHECKBOX, self.EventChecked )
|
|
|
|
self._service_identifiers_to_namespaces[ service_identifier ].append( ( namespace, namespace_checkbox ) )
|
|
|
|
gridbox.AddF( text, FLAGS_MIXED )
|
|
gridbox.AddF( namespace_checkbox, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
hbox.AddF( gridbox, FLAGS_MIXED )
|
|
|
|
|
|
vbox.AddF( hbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox.AddF( wx.StaticText( panel, label = self._info_string ), FLAGS_MIXED )
|
|
hbox.AddF( vbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
panel.SetSizer( hbox )
|
|
|
|
else:
|
|
|
|
vbox.AddF( wx.StaticText( panel, label = 'no tag repositories' ), FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
panel.SetSizer( vbox )
|
|
|
|
|
|
|
|
def GetInfo( self ):
|
|
|
|
service_identifiers = [ self._checkboxes_to_service_identifiers[ checkbox ] for checkbox in self._checkboxes_to_service_identifiers.keys() if checkbox.GetValue() ]
|
|
|
|
result = []
|
|
|
|
for service_identifier in service_identifiers:
|
|
|
|
good_namespaces = []
|
|
|
|
if service_identifier in self._service_identifiers_to_namespaces:
|
|
|
|
namespaces = self._service_identifiers_to_namespaces[ service_identifier ]
|
|
|
|
for ( namespace, namespace_checkbox ) in namespaces:
|
|
|
|
if namespace_checkbox.GetValue(): good_namespaces.append( namespace )
|
|
|
|
|
|
|
|
result.append( ( service_identifier, good_namespaces ) )
|
|
|
|
|
|
return result
|
|
|
|
|
|
def EventChecked( self, event ):
|
|
|
|
wx.PostEvent( self, wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'advanced_tag_options_changed' ) ) )
|
|
|
|
event.Skip()
|
|
|
|
|
|
class TagsBox( ListBox ):
|
|
|
|
def _GetNamespaceColours( self ): return self._options[ 'namespace_colours' ]
|
|
|
|
def _GetTextColour( self, tag_string ):
|
|
|
|
namespace_colours = self._GetNamespaceColours()
|
|
|
|
if ':' in tag_string:
|
|
|
|
( namespace, sub_tag ) = tag_string.split( ':', 1 )
|
|
|
|
if namespace.startswith( '-' ): namespace = namespace[1:]
|
|
if namespace.startswith( '(+) ' ): namespace = namespace[4:]
|
|
if namespace.startswith( '(-) ' ): namespace = namespace[4:]
|
|
if namespace.startswith( '(X) ' ): namespace = namespace[4:]
|
|
|
|
if namespace in namespace_colours: ( r, g, b ) = namespace_colours[ namespace ]
|
|
else: ( r, g, b ) = namespace_colours[ None ]
|
|
|
|
else: ( r, g, b ) = namespace_colours[ '' ]
|
|
|
|
return ( r, g, b )
|
|
|
|
|
|
class TagsBoxActiveOnly( TagsBox ):
|
|
|
|
def __init__( self, parent, callable ):
|
|
|
|
TagsBox.__init__( self, parent )
|
|
|
|
self._callable = callable
|
|
|
|
self._matches = {}
|
|
|
|
|
|
def _Activate( self, tag ): self._callable( tag )
|
|
|
|
def SetTags( self, matches ):
|
|
|
|
if matches != self._matches:
|
|
|
|
self._matches = matches
|
|
|
|
self._ordered_strings = []
|
|
self._strings_to_terms = {}
|
|
|
|
for ( tag, count ) in matches:
|
|
|
|
if count is None: tag_string = tag
|
|
else: tag_string = tag + ' (' + HC.ConvertIntToPrettyString( count ) + ')'
|
|
|
|
self._ordered_strings.append( tag_string )
|
|
self._strings_to_terms[ tag_string ] = tag
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
if len( matches ) > 0: self._Select( 0 )
|
|
|
|
|
|
|
|
class TagsBoxCPP( TagsBox ):
|
|
|
|
def __init__( self, parent, page_key ):
|
|
|
|
TagsBox.__init__( self, parent, min_height = 200 )
|
|
|
|
self._sort = self._options[ 'default_tag_sort' ]
|
|
|
|
self._page_key = page_key
|
|
|
|
self._tag_service_identifier = CC.NULL_SERVICE_IDENTIFIER
|
|
self._last_media = None
|
|
|
|
self._current_tags_to_count = {}
|
|
self._pending_tags_to_count = {}
|
|
self._petitioned_tags_to_count = {}
|
|
|
|
HC.pubsub.sub( self, 'SetTagsByMedia', 'new_tags_selection' )
|
|
HC.pubsub.sub( self, 'ChangeTagRepository', 'change_tag_repository' )
|
|
|
|
|
|
def _Activate( self, tag ): HC.pubsub.pub( 'add_predicate', self._page_key, tag )
|
|
|
|
def _SortTags( self ):
|
|
|
|
if self._sort == CC.SORT_BY_LEXICOGRAPHIC_ASC: compare_function = lambda a, b: cmp( a, b )
|
|
elif self._sort == CC.SORT_BY_LEXICOGRAPHIC_DESC: compare_function = lambda a, b: cmp( b, a )
|
|
elif self._sort in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_DESC ):
|
|
|
|
tags_to_count = collections.defaultdict( lambda: 0 )
|
|
|
|
tags_to_count.update( self._current_tags_to_count )
|
|
for ( tag, count ) in self._pending_tags_to_count.items(): tags_to_count[ tag ] += count
|
|
|
|
if self._sort == CC.SORT_BY_INCIDENCE_ASC: compare_function = lambda a, b: cmp( ( tags_to_count[ self._strings_to_terms[ a ] ], a ), ( tags_to_count[ self._strings_to_terms[ b ] ], b ) )
|
|
elif self._sort == CC.SORT_BY_INCIDENCE_DESC: compare_function = lambda a, b: cmp( ( tags_to_count[ self._strings_to_terms[ b ] ], a ), ( tags_to_count[ self._strings_to_terms[ a ] ], b ) )
|
|
|
|
|
|
self._ordered_strings.sort( compare_function )
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def ChangeTagRepository( self, page_key, service_identifier ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
self._tag_service_identifier = service_identifier
|
|
|
|
if self._last_media is not None: self.SetTagsByMedia( self._page_key, self._last_media )
|
|
|
|
|
|
|
|
def SetSort( self, sort ):
|
|
|
|
self._sort = sort
|
|
|
|
self._SortTags()
|
|
|
|
|
|
def SetTags( self, current_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ):
|
|
|
|
if current_tags_to_count != self._current_tags_to_count or pending_tags_to_count != self._pending_tags_to_count or petitioned_tags_to_count != self._petitioned_tags_to_count:
|
|
|
|
self._current_tags_to_count = current_tags_to_count
|
|
self._pending_tags_to_count = pending_tags_to_count
|
|
self._petitioned_tags_to_count = petitioned_tags_to_count
|
|
|
|
all_tags = { tag for tag in self._current_tags_to_count.keys() + self._pending_tags_to_count.keys() + self._petitioned_tags_to_count.keys() }
|
|
|
|
self._ordered_strings = []
|
|
self._strings_to_terms = {}
|
|
|
|
for tag in all_tags:
|
|
|
|
tag_string = tag
|
|
|
|
if tag in self._current_tags_to_count: tag_string += ' (' + HC.ConvertIntToPrettyString( self._current_tags_to_count[ tag ] ) + ')'
|
|
if tag in self._pending_tags_to_count: tag_string += ' (+' + HC.ConvertIntToPrettyString( self._pending_tags_to_count[ tag ] ) + ')'
|
|
if tag in self._petitioned_tags_to_count: tag_string += ' (-' + HC.ConvertIntToPrettyString( self._petitioned_tags_to_count[ tag ] ) + ')'
|
|
|
|
self._ordered_strings.append( tag_string )
|
|
self._strings_to_terms[ tag_string ] = tag
|
|
|
|
|
|
self._SortTags()
|
|
|
|
|
|
|
|
def SetTagsByMedia( self, page_key, media ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
self._last_media = media
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = CC.GetMediasTagCount( media, self._tag_service_identifier )
|
|
|
|
self.SetTags( current_tags_to_count, pending_tags_to_count, petitioned_tags_to_count )
|
|
|
|
|
|
|
|
class TagsBoxCPPWithSorter( StaticBox ):
|
|
|
|
def __init__( self, parent, page_key ):
|
|
|
|
StaticBox.__init__( self, parent, 'selection tags' )
|
|
|
|
self._options = wx.GetApp().Read( 'options' )
|
|
|
|
self._sorter = wx.Choice( self )
|
|
|
|
self._sorter.Append( 'lexicographic (a-z)', CC.SORT_BY_LEXICOGRAPHIC_ASC )
|
|
self._sorter.Append( 'lexicographic (z-a)', CC.SORT_BY_LEXICOGRAPHIC_DESC )
|
|
self._sorter.Append( 'incidence (desc)', CC.SORT_BY_INCIDENCE_DESC )
|
|
self._sorter.Append( 'incidence (asc)', CC.SORT_BY_INCIDENCE_ASC )
|
|
|
|
if self._options[ 'default_tag_sort' ] == CC.SORT_BY_LEXICOGRAPHIC_ASC: self._sorter.Select( 0 )
|
|
elif self._options[ 'default_tag_sort' ] == CC.SORT_BY_LEXICOGRAPHIC_DESC: self._sorter.Select( 1 )
|
|
elif self._options[ 'default_tag_sort' ] == CC.SORT_BY_INCIDENCE_DESC: self._sorter.Select( 2 )
|
|
elif self._options[ 'default_tag_sort' ] == CC.SORT_BY_INCIDENCE_ASC: self._sorter.Select( 3 )
|
|
|
|
self._sorter.Bind( wx.EVT_CHOICE, self.EventSort )
|
|
|
|
self._tags_box = TagsBoxCPP( self, page_key )
|
|
|
|
self.AddF( self._sorter, FLAGS_EXPAND_PERPENDICULAR )
|
|
self.AddF( self._tags_box, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
def EventSort( self, event ):
|
|
|
|
selection = self._sorter.GetSelection()
|
|
|
|
if selection != wx.NOT_FOUND:
|
|
|
|
sort = self._sorter.GetClientData( selection )
|
|
|
|
self._tags_box.SetSort( sort )
|
|
|
|
|
|
|
|
class TagsBoxFlat( TagsBox ):
|
|
|
|
def __init__( self, parent, removed_callable ):
|
|
|
|
TagsBox.__init__( self, parent )
|
|
|
|
self._removed_callable = removed_callable
|
|
|
|
|
|
def _RecalcTags( self ):
|
|
|
|
self._ordered_strings = self._strings_to_terms.values()
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def _Activate( self, tag ):
|
|
|
|
del self._strings_to_terms[ tag ]
|
|
|
|
self._RecalcTags()
|
|
|
|
self._removed_callable( tag )
|
|
|
|
|
|
def AddTag( self, tag ):
|
|
|
|
self._strings_to_terms[ tag ] = tag
|
|
|
|
self._RecalcTags()
|
|
|
|
|
|
def GetTags( self ): return self._strings_to_terms.values()
|
|
|
|
def SetTags( self, tags ):
|
|
|
|
self._strings_to_terms = { t : t for t in tags }
|
|
|
|
self._RecalcTags()
|
|
|
|
|
|
class TagsBoxManage( TagsBox ):
|
|
|
|
def __init__( self, parent, callable, current_tags, deleted_tags, pending_tags, petitioned_tags ):
|
|
|
|
TagsBox.__init__( self, parent )
|
|
|
|
self._callable = callable
|
|
|
|
self._show_deleted_tags = False
|
|
|
|
self._current_tags = set( current_tags )
|
|
self._deleted_tags = set( deleted_tags )
|
|
self._pending_tags = set( pending_tags )
|
|
self._petitioned_tags = set( petitioned_tags )
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
def _Activate( self, tag ): self._callable( tag )
|
|
|
|
def _RebuildTagStrings( self ):
|
|
|
|
if self._show_deleted_tags: all_tags = self._current_tags | self._deleted_tags | self._pending_tags | self._petitioned_tags
|
|
else: all_tags = self._current_tags | self._pending_tags | self._petitioned_tags
|
|
|
|
self._ordered_strings = []
|
|
self._strings_to_terms = {}
|
|
|
|
for tag in all_tags:
|
|
|
|
if tag in self._petitioned_tags: tag_string = '(-) ' + tag
|
|
elif tag in self._current_tags: tag_string = tag
|
|
elif tag in self._pending_tags: tag_string = '(+) ' + tag
|
|
else: tag_string = '(X) ' + tag
|
|
|
|
self._ordered_strings.append( tag_string )
|
|
self._strings_to_terms[ tag_string ] = tag
|
|
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def PetitionTag( self, tag ):
|
|
|
|
self._petitioned_tags.add( tag )
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
def PendTag( self, tag ):
|
|
|
|
self._pending_tags.add( tag )
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
def RescindPetition( self, tag ):
|
|
|
|
self._petitioned_tags.discard( tag )
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
def RescindPend( self, tag ):
|
|
|
|
self._pending_tags.discard( tag )
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
def SetShowDeletedTags( self, value ):
|
|
|
|
self._show_deleted_tags = value
|
|
|
|
self._RebuildTagStrings()
|
|
|
|
|
|
class TagsBoxOptions( TagsBox ):
|
|
|
|
def __init__( self, parent, initial_namespace_colours ):
|
|
|
|
TagsBox.__init__( self, parent )
|
|
|
|
self._namespace_colours = dict( initial_namespace_colours )
|
|
|
|
for namespace in self._namespace_colours:
|
|
|
|
if namespace is None: namespace_string = 'default namespace:tag'
|
|
elif namespace == '': namespace_string = 'unnamespaced tag'
|
|
else: namespace_string = namespace + ':tag'
|
|
|
|
self._ordered_strings.append( namespace_string )
|
|
self._strings_to_terms[ namespace_string ] = namespace
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def _Activate( self, tag ): self.RemoveNamespace( tag )
|
|
|
|
def _GetNamespaceColours( self ): return self._namespace_colours
|
|
|
|
def SetNamespaceColour( self, namespace, colour ):
|
|
|
|
if namespace not in self._namespace_colours:
|
|
|
|
namespace_string = namespace + ':tag'
|
|
|
|
self._ordered_strings.append( namespace_string )
|
|
self._strings_to_terms[ namespace_string ] = namespace
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
|
|
self._namespace_colours[ namespace ] = colour.Get()
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def GetNamespaceColours( self ): return self._namespace_colours
|
|
|
|
def GetSelectedNamespaceColour( self ):
|
|
|
|
if self._current_selected_index is not None:
|
|
|
|
namespace_string = self._ordered_strings[ self._current_selected_index ]
|
|
|
|
namespace = self._strings_to_terms[ namespace_string ]
|
|
|
|
( r, g, b ) = self._namespace_colours[ namespace ]
|
|
|
|
colour = wx.Colour( r, g, b )
|
|
|
|
return ( namespace, colour )
|
|
|
|
|
|
return None
|
|
|
|
|
|
def RemoveNamespace( self, namespace ):
|
|
|
|
if namespace is not None and namespace != '':
|
|
|
|
namespace_string = namespace + ':tag'
|
|
|
|
self._ordered_strings.remove( namespace_string )
|
|
|
|
del self._strings_to_terms[ namespace_string ]
|
|
|
|
del self._namespace_colours[ namespace ]
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
|
|
class TagsBoxPredicates( TagsBox ):
|
|
|
|
def __init__( self, parent, page_key, initial_predicates = [] ):
|
|
|
|
TagsBox.__init__( self, parent, min_height = 100 )
|
|
|
|
self._page_key = page_key
|
|
|
|
if len( initial_predicates ) > 0:
|
|
|
|
for predicate in initial_predicates:
|
|
|
|
self._ordered_strings.append( predicate )
|
|
self._strings_to_terms[ predicate ] = predicate
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
|
|
def _Activate( self, tag ): HC.pubsub.pub( 'remove_predicate', self._page_key, tag )
|
|
|
|
def ActivatePredicate( self, tag ):
|
|
|
|
if tag in self._ordered_strings:
|
|
|
|
self._ordered_strings.remove( tag )
|
|
del self._strings_to_terms[ tag ]
|
|
|
|
else:
|
|
|
|
if tag == 'system:inbox' and 'system:archive' in self._ordered_strings: self._ordered_strings.remove( 'system:archive' )
|
|
elif tag == 'system:archive' and 'system:inbox' in self._ordered_strings: self._ordered_strings.remove( 'system:inbox' )
|
|
elif tag == 'system:local' and 'system:not local' in self._ordered_strings: self._ordered_strings.remove( 'system:not local' )
|
|
elif tag == 'system:not local' and 'system:local' in self._ordered_strings: self._ordered_strings.remove( 'system:local' )
|
|
|
|
self._ordered_strings.append( tag )
|
|
self._strings_to_terms[ tag ] = tag
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def AddPredicate( self, predicate ):
|
|
|
|
self._ordered_strings.append( predicate )
|
|
self._strings_to_terms[ predicate ] = predicate
|
|
|
|
self._ordered_strings.sort()
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|
|
def GetPredicates( self ): return self._ordered_strings
|
|
|
|
def HasPredicate( self, predicate ): return predicate in self._ordered_strings
|
|
|
|
def RemovePredicate( self, predicate ):
|
|
|
|
self._ordered_strings.remove( predicate )
|
|
del self._strings_to_terms[ predicate ]
|
|
|
|
self._TextsHaveChanged()
|
|
|
|
|