NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

window.py (39534B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\chat\client\window.py
      2 import blue
      3 from bannedwords.client import bannedwords
      4 import gametime
      5 from carbon.common.script.util.format import GetTimeParts
      6 from inventorycommon.const import ownerSystem
      7 import threadutils
      8 import uthread2
      9 import logging
     10 from carbon.common.script.sys.serviceConst import ROLE_LEGIONEER, ROLE_CENTURION, ROLEMASK_ELEVATEDPLAYER, ROLE_GML
     11 import carbonui
     12 from carbonui import Align, PickState, AxisAlignment
     13 from carbonui.uianimations import animations
     14 from carbonui.uicore import uicore
     15 from carbonui.control.window import Window
     16 from clientsettings.setting import UserSettingBool, UserSettingEnum
     17 from carbonui.primitives.container import Container
     18 from carbonui.primitives.containerAutoSize import ContainerAutoSize
     19 from carbonui.primitives.fill import Fill
     20 from carbonui.primitives.sprite import Sprite
     21 from carbonui.decorative.divider_line import DividerLine
     22 from carbonui.control.basicDynamicScroll import BasicDynamicScroll
     23 from carbonui.control.scrollentries import ScrollEntryNode
     24 from carbonui.text.settings import get_font_size_for_preset
     25 from carbonui.fontconst import EVE_SMALL_FONTSIZE
     26 from carbonui.control.contextMenu.menuData import MenuData
     27 from carbonui.control.contextMenu.menuDataFactory import CreateMenuDataFromRawTuples
     28 from carbonui.window.settings import GetRegisteredState
     29 from eve.client.script.ui.control.divider import Divider
     30 from eve.client.script.ui.control.eveEditPlainText import EditPlainText
     31 from eve.client.script.ui.control.eveLabel import EveLabelSmall
     32 from eve.client.script.ui.util.linkUtil import GetCharIDFromTextLink
     33 from eve.client.script.ui.control.allUserEntries import AllUserEntries
     34 from eve.client.script.ui.control.entries.util import GetFromClass
     35 from eve.client.script.ui.shared.comtool.filterSettings import ChatFilterSettings
     36 from eve.common.script.sys.idCheckers import IsEvePlayerCharacter
     37 import eveicon
     38 from menu import MenuLabel
     39 import localization
     40 from chatutil.filter import CleanText, ShouldHightlightBlink
     41 from chat.client.chat_settings import default_light_background_setting, default_message_mode_setting, default_compact_member_entries_setting, default_show_member_list_setting, default_font_size_setting, global_font_size_setting, elevated_chat_highlighting_setting, show_message_timestamp_setting, highlight_my_messages_setting, ApplyFontSizeGloballySetting, EffectiveFontSizeSetting, auto_collapse_messages, auto_collapse_message_lines
     42 from chat.client.const import AVAILABLE_FONT_SIZES, MAX_MESSAGE_LENGTH, MAX_MESSAGES_IN_HISTORY, SLASH_EMOTE_STRING, SLASH_ME_STRING, CHAT_MESSAGE_MODE_LABEL_PATH
     43 from chat.client.util import get_chat_default_height, get_chat_default_width, parse_pasted_text, parse_chat_message, truncate_chat_message, check_banned_message_in_new_player_channel
     44 from chat.client.window_stack import ChatWindowStack
     45 from chat.client.message_list_entry import ChatMessageListEntry
     46 from chat.client.member_list_entry import ChatMemberListEntry, ChatMemberCompactListEntry
     47 from chat.client.slash_command import handle_slash_command
     48 from chat.client.hide_messages import open_hide_message_settings, has_hide_message_setting
     49 logger = logging.getLogger('chat')
     50 
     51 class BaseChatWindow(Window):
     52     default_caption = 'Chat'
     53     default_windowID = 'ChatWindow'
     54     default_stackID = 'ChatWindowStack'
     55     default_iconNum = 'res:/ui/Texture/WindowIcons/chatchannel.png'
     56     default_left = 16
     57     default_isKillable = False
     58     default_minSize = (120, 120)
     59     default_isCompact = True
     60     default_apply_content_padding = False
     61 
     62     @staticmethod
     63     def default_top():
     64         return uicore.desktop.height - ChatWindow.default_height() - 16
     65 
     66     @staticmethod
     67     def default_width():
     68         return get_chat_default_width()
     69 
     70     @staticmethod
     71     def default_height():
     72         return get_chat_default_height()
     73 
     74     @staticmethod
     75     def default_isLightBackground():
     76         return default_light_background_setting.get()
     77 
     78     @classmethod
     79     def should_open_stacked_window_as_minimized(cls, window_id):
     80         stack_id = cls.GetRegisteredOrDefaultStackID(windowID=window_id)
     81         return GetRegisteredState(stack_id, 'minimized')
     82 
     83     def GetStackClass(self):
     84         return ChatWindowStack
     85 
     86     def PostApplyAttributes(self, attributes):
     87         attributes['openMinimized'] = self.should_open_stacked_window_as_minimized(attributes.windowID) if attributes.get('check_open_minimized', False) else False
     88         Window.PostApplyAttributes(self, attributes)
     89 
     90 
     91 class ChatWindow(BaseChatWindow):
     92     __notifyevents__ = ['OnPortraitCreated']
     93     _controller = None
     94     _member_list_entries = {}
     95     _pending_message_entries = {}
     96     _pending_portrait_reloads = set()
     97     _previous_inputs = ['']
     98     _previous_input_index = None
     99     _highlighted_text = ''
    100     _member_update_lock = None
    101     _message_update_lock = None
    102 
    103     def ApplyAttributes(self, attributes):
    104         self._controller = attributes.controller
    105         self._member_update_lock = uthread2.Semaphore()
    106         self._message_update_lock = uthread2.Semaphore()
    107         super(ChatWindow, self).ApplyAttributes(attributes)
    108         self._setup_settings()
    109         self._layout()
    110         self._register()
    111         uthread2.start_tasklet(self._update_member_count)
    112         uthread2.start_tasklet(self._setup_members_list, force_reload=True)
    113         uthread2.start_tasklet(self._reload_messages)
    114 
    115     def CloseByUser(self, *args):
    116         if self.closing and not self.killable:
    117             return
    118         super(ChatWindow, self).CloseByUser(*args)
    119         self._controller.close_by_user()
    120 
    121     def Close(self, *args, **kwargs):
    122         if self.closing:
    123             return
    124         self.unregister()
    125         if sm.GetService('focus').GetFocusChannel() == self:
    126             sm.GetService('focus').SetFocusChannel()
    127         self._controller = None
    128         super(ChatWindow, self).Close(*args, **kwargs)
    129 
    130     def _register(self):
    131         sm.RegisterNotify(self)
    132         self._controller.on_members_set.connect(self._on_members_set)
    133         self._controller.on_members_cleared.connect(self._on_members_cleared)
    134         self._controller.on_member_added.connect(self._on_member_added)
    135         self._controller.on_member_removed.connect(self._on_member_removed)
    136         self._controller.on_member_data_changed.connect(self._on_member_data_changed)
    137         self._controller.on_message_added.connect(self._on_message_added)
    138         self._controller.on_messages_cleared.connect(self._on_messages_cleared)
    139         self._controller.on_pending_message_removed.connect(self._on_pending_message_removed)
    140 
    141     def unregister(self):
    142         sm.UnregisterNotify(self)
    143         if self._controller:
    144             self._controller.on_members_set.disconnect(self._on_members_set)
    145             self._controller.on_members_cleared.disconnect(self._on_members_cleared)
    146             self._controller.on_member_added.disconnect(self._on_member_added)
    147             self._controller.on_member_removed.disconnect(self._on_member_removed)
    148             self._controller.on_member_data_changed.disconnect(self._on_member_data_changed)
    149             self._controller.on_message_added.disconnect(self._on_message_added)
    150             self._controller.on_messages_cleared.disconnect(self._on_messages_cleared)
    151             self._controller.on_pending_message_removed.disconnect(self._on_pending_message_removed)
    152         global_font_size_setting.on_change.disconnect(self._on_global_font_size_setting_changed)
    153         elevated_chat_highlighting_setting.on_change.disconnect(self._on_elevated_chat_highlighting_setting_changed)
    154         show_message_timestamp_setting.on_change.disconnect(self._on_show_message_timestamp_setting_changed)
    155 
    156     def _on_members_set(self):
    157         self._on_members_cleared()
    158         self._update_members()
    159 
    160     def _on_members_cleared(self):
    161         if not self._controller:
    162             return
    163         self._update_member_count()
    164         self._member_list_entries.clear()
    165         self._member_list_scroll.Clear()
    166 
    167     def _on_member_added(self, character_id):
    168         self._update_members()
    169 
    170     def _on_member_removed(self, character_id):
    171         self._update_members()
    172 
    173     @threadutils.threaded
    174     def _on_member_data_changed(self, character_id, data):
    175         entry = self._member_list_entries.get(character_id, None)
    176         if entry:
    177             entry.corpID = data.get('corporation_id', None)
    178             entry.allianceID = data.get('alliance_id', None)
    179             entry.warFactionID = data.get('war_faction_id', None)
    180             if entry.panel:
    181                 entry.panel.UpdateRelationship(character_id)
    182 
    183     def _on_messages_cleared(self):
    184         with self._message_update_lock:
    185             if not self._controller:
    186                 return
    187             self._pending_message_entries.clear()
    188             self._messages_scroll.Clear()
    189 
    190     def _on_message_added(self, sender_id, text, timestamp, color):
    191         if sender_id == session.charid:
    192             return
    193         with self._message_update_lock:
    194             if not self._controller:
    195                 return
    196             self._construct_new_message_entry(sender_id, text, timestamp, color)
    197 
    198     def _on_pending_message_removed(self, message_id, was_delivered):
    199         if not self._controller:
    200             return
    201         message_entry = self._pending_message_entries.pop(message_id, None)
    202         if message_entry and not was_delivered:
    203             ChatMessageListEntry.MarkMessageUndelivered(message_entry)
    204 
    205     @threadutils.throttled(0.2)
    206     def _reload_messages(self):
    207         with self._message_update_lock:
    208             if not self._controller:
    209                 return
    210             portion = self._messages_scroll.GetScrollProportion()
    211             self._messages_scroll.Clear()
    212             nodes = []
    213             for sender, text, timestamp, color in self._controller.messages:
    214                 node = self._construct_message_entry(sender, text, timestamp, color)
    215                 nodes.append(node)
    216 
    217             self._messages_scroll.AddNodes(-1, nodes)
    218             if portion:
    219                 self._messages_scroll.ScrollToProportion(portion)
    220 
    221     def _construct_new_message_entry(self, sender_id, text, timestamp, color):
    222         message_nodes = self._messages_scroll.GetNodes()
    223         if message_nodes and len(message_nodes) >= MAX_MESSAGES_IN_HISTORY:
    224             self._messages_scroll.RemoveNodes([message_nodes[0]])
    225         entry = self._construct_message_entry(sender_id, text, timestamp, color)
    226         self._messages_scroll.AddNodes(-1, [entry])
    227         self._blink_channel_if_needed(sender_id, text)
    228         return entry
    229 
    230     def _construct_message_entry(self, sender_id, text, timestamp, color):
    231         try:
    232             character_id = int(sender_id)
    233         except:
    234             character_id = sender_id
    235 
    236         if elevated_chat_highlighting_setting.is_enabled():
    237             highlight_func = self._highlight_message
    238             collect_words_in_stack = True
    239         else:
    240             highlight_func = None
    241             collect_words_in_stack = False
    242         if auto_collapse_messages.get() and character_id and character_id != session.charid and IsEvePlayerCharacter(character_id):
    243             maxLines = int(auto_collapse_message_lines.get())
    244         else:
    245             maxLines = None
    246         node = ScrollEntryNode(decoClass=ChatMessageListEntry, text=text, timestamp=timestamp, fontsize=self._get_font_size(), letterspace=self._get_letter_space(), sender=sender_id, charid=character_id, mode=self._message_style_setting.get(), color=color, channelMenu=self._get_channel_context_menu, mouseOverWordCallback=highlight_func, collectWordsInStack=collect_words_in_stack, channelid=self._controller.channel_id, maxLines=maxLines, showExpander=False)
    247         return node
    248 
    249     @threadutils.throttled(0.2)
    250     def _reload_members(self):
    251         with self._member_update_lock:
    252             if not self._controller:
    253                 return
    254             self._member_list_scroll.Clear()
    255             self._member_list_entries.clear()
    256             entries = []
    257             member_ids = self._controller.member_ids.copy()
    258             if member_ids:
    259                 cfg.eveowners.Prime(member_ids)
    260             for character_id in member_ids:
    261                 entry = self._construct_member_list_entry(character_id)
    262                 if not entry:
    263                     continue
    264                 entries.append(entry)
    265 
    266             sorted_entries = sorted(entries, key=lambda x: x.charIndex)
    267             self._member_list_scroll.AddNodes(-1, sorted_entries)
    268 
    269     @threadutils.throttled(0.2)
    270     def _update_members(self):
    271         self._update_member_count()
    272         with self._member_update_lock:
    273             if not self._controller:
    274                 return
    275             current_rendered_ids = set(self._member_list_entries.keys())
    276             removed_character_ids = current_rendered_ids - self._controller.member_ids
    277             added_character_ids = self._controller.member_ids - current_rendered_ids
    278             self._remove_members(removed_character_ids)
    279             if added_character_ids:
    280                 cfg.eveowners.Prime(added_character_ids)
    281                 self._add_members(added_character_ids)
    282 
    283     def _remove_members(self, character_ids):
    284         entries = []
    285         for character_id in character_ids:
    286             entry = self._member_list_entries.pop(character_id, None)
    287             if entry:
    288                 entries.append(entry)
    289 
    290         if entries:
    291             self._member_list_scroll.RemoveNodes(entries)
    292 
    293     def _add_members(self, character_ids):
    294         for character_id in character_ids:
    295             self._construct_new_member_list_entry(character_id)
    296 
    297     def _construct_new_member_list_entry(self, character_id):
    298         if self.destroyed or character_id in self._member_list_entries or character_id not in self._controller.member_ids:
    299             return
    300         entry = self._construct_member_list_entry(character_id)
    301         if not entry:
    302             return
    303         idx = GetIdxForUser_WithBinarySearch(self._member_list_scroll, entry.charIndex)
    304         self._member_list_scroll.AddNodes(idx, [entry], updateScroll=False)
    305         entry.positionFromTop = idx * entry.height
    306         self._update_member_list_scroll_position()
    307 
    308     def _construct_member_list_entry(self, character_id):
    309         owner_info = cfg.eveowners.GetIfExists(character_id)
    310         if not owner_info:
    311             logger.warning('_construct_member_list_entry not constructing for character_id %s as it was not in cfg.eveowners', character_id)
    312             return
    313         if self._compact_member_entries_setting.is_enabled():
    314             entry_type = ChatMemberCompactListEntry
    315         else:
    316             entry_type = ChatMemberListEntry
    317         member_data = self._controller.get_member_data(character_id)
    318         entry = GetFromClass(entry_type, {'channelID': self._controller.channel_id,
    319          'charID': character_id,
    320          'info': owner_info,
    321          'corpID': member_data.get('corporation_id', None),
    322          'allianceID': member_data.get('alliance_id', None),
    323          'warFactionID': member_data.get('war_faction_id', None),
    324          'showChatBubble': not self._compact_member_entries_setting.is_enabled(),
    325          'charIndex': owner_info.name.lower()})
    326         self._member_list_entries[character_id] = entry
    327         return entry
    328 
    329     @threadutils.throttled(0.2)
    330     def _update_member_count(self):
    331         if not self._controller:
    332             return
    333         member_count = self._controller.member_count
    334         if member_count > 1:
    335             self._num_members_label.text = str(member_count)
    336             self._num_members_container.display = True
    337             self.caption = u'{} [{}]'.format(self._controller.window_display_name, member_count)
    338         else:
    339             self._num_members_container.display = False
    340             self.caption = self._controller.window_display_name
    341 
    342     def _update_min_size(self):
    343         if self._member_list_container is None:
    344             return
    345         default_min_width, default_min_height = self.default_minSize
    346         user_list_width, _ = self.GetWindowSizeForContentSize(width=self._member_list_container.width)
    347         min_width = max(default_min_width, user_list_width)
    348         self.SetMinSize((min_width, default_min_height))
    349 
    350     def _handle_input_on_return(self):
    351         text = parse_chat_message(self._input_field.GetValue(html=0), truncate=False)
    352         if not text:
    353             return
    354         bannedwords.check_chat_character_allowed(session.charid)
    355         bannedwords.check_chat_words_allowed(text)
    356         self._input_field.SetValue('')
    357         if self._previous_inputs[-1] != text:
    358             self._previous_inputs.append(text)
    359         self._previous_input_index = None
    360         if text.startswith(SLASH_ME_STRING):
    361             text = text.replace(SLASH_ME_STRING, SLASH_EMOTE_STRING, 1)
    362         if text.startswith('/') and not (text.startswith(SLASH_EMOTE_STRING) or text == '/'):
    363             handle_slash_command(text, channel_controller=self._controller)
    364             return
    365         text = truncate_chat_message(text)
    366         if text == '':
    367             return
    368         if not self._controller.can_send_chat_message():
    369             return
    370         check_banned_message_in_new_player_channel(self._controller.category_id, text)
    371         timestamp = gametime.GetWallclockTime()
    372         color = self._controller.get_color_for_sender(session.charid)
    373         entry = self._construct_new_message_entry(session.charid, text, timestamp, color)
    374         message_id = self._controller.send_chat_message(text)
    375         if message_id:
    376             self._pending_message_entries[message_id] = entry
    377 
    378     @threadutils.throttled(0.5)
    379     def _update_member_list_scroll_position(self):
    380         if not self._controller:
    381             return
    382         width, _ = self._member_list_scroll.sr.clipper.GetAbsoluteSize()
    383         with self._member_list_scroll.KillUpdateThreadAndBlock():
    384             self._member_list_scroll.UpdateNodesWidthAndPosition(width)
    385             self._member_list_scroll.UpdatePosition(fromWhere='AddNodes')
    386 
    387     def _setup_settings(self):
    388         self._message_style_setting = UserSettingEnum(settings_key='%s_mode' % self.name, default_value=lambda : default_message_mode_setting.get(), options=default_message_mode_setting.options)
    389         self._font_size_setting = UserSettingEnum(settings_key='chatfontsize_%s' % self.name, default_value=lambda : default_font_size_setting.get(), options=AVAILABLE_FONT_SIZES)
    390         self._apply_font_size_globally_setting = ApplyFontSizeGloballySetting(font_size_setting=self._font_size_setting)
    391         self._effective_font_size_setting = EffectiveFontSizeSetting(font_size_setting=self._font_size_setting)
    392         self._chat_notifications_enabled_setting = UserSettingBool(settings_key='chatWindowBlink_%s' % self.name, default_value=True)
    393         self._show_member_list_setting = UserSettingBool(settings_key='%s_usermode' % self.name, default_value=lambda : default_show_member_list_setting.get())
    394         self._compact_member_entries_setting = UserSettingBool(settings_key='chatCondensedUserList_%s' % self.name, default_value=lambda : default_compact_member_entries_setting.get())
    395         self._message_style_setting.on_change.connect(self._on_message_style_setting_changed)
    396         self._font_size_setting.on_change.connect(self._on_font_size_setting_changed)
    397         self._apply_font_size_globally_setting.on_change.connect(self._on_apply_font_size_globally_setting_changed)
    398         self._show_member_list_setting.on_change.connect(self._on_show_member_list_setting_changed)
    399         self._compact_member_entries_setting.on_change.connect(self._on_compact_member_entries_setting_changed)
    400         global_font_size_setting.on_change.connect(self._on_global_font_size_setting_changed)
    401         elevated_chat_highlighting_setting.on_change.connect(self._on_elevated_chat_highlighting_setting_changed)
    402         show_message_timestamp_setting.on_change.connect(self._on_show_message_timestamp_setting_changed)
    403 
    404     def _layout(self):
    405         self._construct_input()
    406         self._construct_user_list()
    407         self._construct_message_area()
    408         self._update_min_size()
    409         self.killable = self._controller.is_closable
    410         if elevated_chat_highlighting_setting.is_enabled():
    411             self._input_field.OnKeyUp = self._check_turn_highlight_off
    412             self._messages_scroll.OnKeyUp = self._check_turn_highlight_off
    413 
    414     def _construct_input(self):
    415         input_cont = ContainerAutoSize(parent=self.content, align=Align.TOBOTTOM, alignMode=Align.TOBOTTOM, padTop=-3)
    416         bottom_divider = Divider(name='bottom_divider', parent=input_cont, align=Align.TOTOP_NOPUSH, pickState=PickState.ON, height=8)
    417         self._input_field = EditPlainText(parent=input_cont, align=Align.TOBOTTOM, height=settings.user.ui.Get('chatinputsize_%s' % self.name, 64), innerPadding=(8, 8, 8, 8), padding=(-1, 3, -1, -1), pushContent=True, maxLength=MAX_MESSAGE_LENGTH)
    418         self._input_field.ValidatePaste = parse_pasted_text
    419         self._input_field.OnReturn = self._handle_input_on_return
    420         self._input_field.CtrlUp = self._handle_input_control_up
    421         self._input_field.CtrlDown = self._handle_input_control_down
    422         self._input_field.RegisterFocus = self._RegisterFocus
    423         bottom_divider.Startup(self._input_field, 'height', 'y', 30, 96)
    424         bottom_divider.OnSizeChanged = self._on_input_size_changed
    425 
    426     def _construct_user_list(self):
    427         self._member_list_container = Container(parent=self.content, align=Align.TORIGHT, width=settings.user.ui.Get('%s_userlistwidth' % self.name, 128), clipChildren=True)
    428         DividerLine(parent=self._member_list_container, align=Align.TOLEFT_NOPUSH)
    429         self._user_list_underlay = Fill(bgParent=self._member_list_container, align=Align.TOALL, padLeft=1, color=(1.0, 1.0, 1.0, 0.05))
    430         divider = Divider(name='user_list_divider', parent=self._member_list_container, align=Align.TOLEFT_NOPUSH, pickState=PickState.ON, width=8, cross_axis_alignment=AxisAlignment.START)
    431         divider.Startup(victim=self._member_list_container, attribute='width', xory='x', minValue=50, maxValue=200)
    432         divider.OnSizeChanged = self._user_list_end_scale
    433         self._num_members_container = Container(name='num_members_container', parent=self._member_list_container, align=Align.TOTOP, pickState=PickState.ON, height=16, padding=(8, 8, 8, 0), hint=self._controller.member_count_tooltip)
    434         self._num_members_container.display = False
    435         Sprite(name='num_members_sprite', parent=self._num_members_container, align=Align.CENTERLEFT, pickState=carbonui.PickState.OFF, width=16, height=16, texturePath=eveicon.person, color=carbonui.TextColor.NORMAL)
    436         self._num_members_label = EveLabelSmall(name='num_members_label', parent=self._num_members_container, align=Align.CENTERLEFT, left=20, text='')
    437         self._member_list_scroll = BasicDynamicScroll(name='user_list_scroll', parent=self._member_list_container, align=Align.TOALL, padding=(8, 8, 8, 0), entry_spacing=self._get_user_list_entry_spacing(), pushContent=True)
    438         self._member_list_scroll.isCondensed = self._compact_member_entries_setting.is_enabled()
    439         self._member_list_scroll.GetContentContainer().OnDropData = self._on_drop_to_user_list
    440 
    441     def _construct_message_area(self):
    442         self._messages_scroll = BasicDynamicScroll(parent=self.content, align=Align.TOALL, innerPadding=(8, 8, 8, 8), entry_spacing=4, pushContent=False, stickToBottom=True)
    443         self._messages_scroll.sr.content.GetMenu = self._get_channel_context_menu
    444         self._messages_scroll.Load(contentList=[], fixedEntryHeight=18)
    445 
    446     def _setup_members_list(self, force_reload = False):
    447         if not self._controller:
    448             return
    449         if not self._show_member_list_setting.is_enabled():
    450             self._member_list_container.display = False
    451         else:
    452             minW = 50
    453             maxW = 200
    454             self._member_list_container.width = min(maxW, max(minW, self._member_list_container.width))
    455             self._member_list_container.display = True
    456             condensed = self._compact_member_entries_setting.is_enabled()
    457             if force_reload or condensed != getattr(self._member_list_scroll, 'isCondensed', False):
    458                 self._reload_members()
    459 
    460     def _on_message_style_setting_changed(self, value):
    461         if not self._controller:
    462             return
    463         self._reload_messages()
    464         uicore.registry.SetFocus(self)
    465 
    466     def _on_font_size_setting_changed(self, value):
    467         if self._apply_font_size_globally_setting.is_enabled():
    468             global_font_size_setting.set(self._font_size_setting.get())
    469         else:
    470             self._update_font_size()
    471 
    472     def _on_apply_font_size_globally_setting_changed(self, value):
    473         if self._apply_font_size_globally_setting.is_enabled():
    474             global_font_size_setting.set(self._font_size_setting.get())
    475 
    476     def _on_global_font_size_setting_changed(self, value):
    477         self._update_font_size()
    478 
    479     def _on_elevated_chat_highlighting_setting_changed(self, value):
    480         if elevated_chat_highlighting_setting.is_enabled():
    481             if self._input_field and not self._input_field.destroyed:
    482                 self._input_field.OnKeyUp = self._check_turn_highlight_off
    483             if self._messages_scroll and not self._messages_scroll.destroyed:
    484                 self._messages_scroll.OnKeyUp = self._check_turn_highlight_off
    485         else:
    486             if self._input_field and not self._input_field.destroyed:
    487                 self._input_field.OnKeyUp = None
    488             if self._messages_scroll and not self._messages_scroll.destroyed:
    489                 self._messages_scroll.OnKeyUp = None
    490         self._turn_message_highlight_off()
    491         self._reload_messages()
    492 
    493     def _on_show_member_list_setting_changed(self, value):
    494         self._setup_members_list()
    495 
    496     def _on_compact_member_entries_setting_changed(self, value):
    497         is_compact = self._compact_member_entries_setting.is_enabled()
    498         if self._show_member_list_setting.is_enabled():
    499             self._member_list_scroll.entry_spacing = self._get_user_list_entry_spacing()
    500             self._reload_members()
    501         self._member_list_scroll.isCondensed = is_compact if self._member_list_scroll.display else -1
    502 
    503     def _on_show_message_timestamp_setting_changed(self, value):
    504         self._reload_messages()
    505 
    506     def _on_input_size_changed(self):
    507         settings.user.ui.Set('chatinputsize_%s' % self.name, self._input_field.height)
    508 
    509     def _update_font_size(self):
    510         self._reload_messages()
    511         self._input_field.SetDefaultFontSize(self._get_font_size())
    512 
    513     def _get_font_size(self):
    514         font_size = global_font_size_setting.get()
    515         if font_size is None:
    516             font_size = self._font_size_setting.get()
    517         return font_size
    518 
    519     def _get_letter_space(self):
    520         font_size = self._get_font_size()
    521         small_font_size = get_font_size_for_preset(EVE_SMALL_FONTSIZE)
    522         if font_size <= small_font_size:
    523             return 1
    524         else:
    525             return 0
    526 
    527     def _get_user_list_entry_spacing(self):
    528         if self._compact_member_entries_setting.is_enabled():
    529             return 1
    530         else:
    531             return 4
    532 
    533     def _check_turn_highlight_off(self, *args, **kwargs):
    534         if elevated_chat_highlighting_setting.is_enabled():
    535             self._turn_message_highlight_off()
    536 
    537     def _highlight_message(self, findText = '', *args):
    538         if not self._is_highlighting():
    539             return
    540         if len(findText.strip().replace('(', '').replace(')', '')) < 2:
    541             return
    542         if self._highlighted_text != findText:
    543             self._turn_message_highlight_off()
    544         self._highlighted_text = findText
    545         nodes = self._messages_scroll.GetNodes()
    546         for eachNode in nodes:
    547             if eachNode.panel:
    548                 eachNode.panel.LoadHighlightedText(findText)
    549 
    550     def _turn_message_highlight_off(self):
    551         _highlighted_text = getattr(self, '_highlighted_text', '')
    552         if not _highlighted_text:
    553             return
    554         for eachNode in self._messages_scroll.GetNodes():
    555             if eachNode.panel:
    556                 eachNode.panel.RemoveHighlightedText()
    557 
    558         self._highlighted_text = None
    559 
    560     def _is_highlighting(self, *args):
    561         return elevated_chat_highlighting_setting.is_enabled() and uicore.uilib.Key(carbonui.uiconst.VK_CONTROL)
    562 
    563     def _blink_channel_if_needed(self, sender_id, text):
    564         if sender_id in (session.charid, ownerSystem):
    565             return
    566         blink_enabled = self._chat_notifications_enabled_setting.is_enabled()
    567         if blink_enabled or ShouldHightlightBlink(text):
    568             self.Blink()
    569             if self.state == carbonui.uiconst.UI_HIDDEN or self.IsMinimized():
    570                 self.SetBlinking()
    571 
    572     def _get_channel_context_menu(self, *args):
    573         m = [(MenuLabel('UI/Common/CopyAll'), self._copy_all_messages)]
    574         return m
    575 
    576     def _copy_all_messages(self):
    577         t = ''
    578         for node in self._messages_scroll.GetNodes():
    579             timestr = ''
    580             if show_message_timestamp_setting.is_enabled():
    581                 year, month, wd, day, hour, min, sec, ms = GetTimeParts(node.timestamp)
    582                 timestr = '[%02d:%02d:%02d] ' % (hour, min, sec)
    583             who = node.sender
    584             try:
    585                 who = cfg.eveowners.Get(who).name
    586             except ValueError:
    587                 pass
    588 
    589             text = node.text.replace('&gt;', '>').replace('&amp;', '&')
    590             text = CleanText(text)
    591             t += '%s%s > %s\r\n' % (timestr, who, text)
    592 
    593         blue.pyos.SetClipboardData(t)
    594 
    595     def _handle_input_control_down(self, editctrl):
    596         self._browse_inputs(1)
    597 
    598     def _handle_input_control_up(self, editctrl):
    599         self._browse_inputs(-1)
    600 
    601     def _browse_inputs(self, updown):
    602         if self._previous_input_index is None:
    603             self._previous_input_index = len(self._previous_inputs) - 1
    604         else:
    605             self._previous_input_index += updown
    606         if self._previous_input_index < 0:
    607             self._previous_input_index = len(self._previous_inputs) - 1
    608         elif self._previous_input_index >= len(self._previous_inputs):
    609             self._previous_input_index = 0
    610         self._input_field.SetValue(self._previous_inputs[self._previous_input_index], cursorPos=-1)
    611 
    612     def _user_list_end_scale(self, *args):
    613         settings.user.ui.Set('%s_userlistwidth' % self.name, self._member_list_container.width)
    614         self._update_min_size()
    615 
    616     def _on_drop_to_user_list(self, dragObj, nodes):
    617         for node in nodes[:5]:
    618             character_id = GetCharIDFromTextLink(node)
    619             if not character_id:
    620                 guid = getattr(node, '__guid__', None)
    621                 if guid not in AllUserEntries():
    622                     return
    623                 character_id = node.charID
    624             if self._controller.can_invite_character(character_id):
    625                 self._controller.invite_character(character_id)
    626 
    627     def _RegisterFocus(self, *args, **kwargs):
    628         sm.GetService('focus').SetFocusChannel(self)
    629 
    630     def SetCharFocus(self, char):
    631         uicore.registry.SetFocus(self._input_field)
    632         uicore.layer.menu.Flush()
    633         if char is not None:
    634             self._input_field.OnChar(char, 0)
    635 
    636     def OnTabSelect(self):
    637         uicore.registry.SetFocus(self._input_field)
    638         self._reload_pending_portaits()
    639 
    640     def OnPortraitCreated(self, charID, _size):
    641         if self.destroyed or self._messages_scroll is None:
    642             return
    643         self._pending_portrait_reloads.add(charID)
    644         if not self.display:
    645             return
    646         self._reload_pending_portaits()
    647 
    648     def _reload_pending_portaits(self):
    649         if not self._pending_portrait_reloads:
    650             return
    651         if self._show_member_list_setting.is_enabled():
    652             for character_id in self._pending_portrait_reloads:
    653                 entry = self._member_list_entries.get(character_id, None)
    654                 if entry and entry.panel and not entry.panel.picloaded:
    655                     entry.panel.LoadPortrait(orderIfMissing=False)
    656 
    657         for entry in self._messages_scroll.GetNodes():
    658             if not entry.panel or not entry.panel.display:
    659                 continue
    660             if entry.charid in self._pending_portrait_reloads and not entry.panel.picloaded:
    661                 entry.panel.LoadPortrait(orderIfMissing=False)
    662 
    663         self._pending_portrait_reloads.clear()
    664 
    665     def OnSetActive_(self, *args):
    666         if self._user_list_underlay is not None:
    667             animations.FadeTo(self._user_list_underlay, startVal=self._user_list_underlay.opacity, endVal=0.05, duration=0.1)
    668 
    669     def OnSetInactive(self, *args):
    670         super(ChatWindow, self).OnSetInactive()
    671         if self._user_list_underlay is not None:
    672             animations.FadeOut(self._user_list_underlay, duration=0.3)
    673 
    674     def OnMouseUp(self, _):
    675         self.StackSelf()
    676 
    677     @threadutils.threaded
    678     def StackSelf(self):
    679         if self.stacked:
    680             self.stack.Check()
    681         elif self.FindWindowToStackTo():
    682             pass
    683         else:
    684             self._ConstructStack([(self, 1)], [])
    685 
    686     def SetBlinking(self):
    687         super(ChatWindow, self).SetBlinking()
    688         sm.ScatterEvent('OnChatWindowStartBlinking', self)
    689 
    690     def SetNotBlinking(self):
    691         super(ChatWindow, self).SetNotBlinking()
    692         sm.ScatterEvent('OnChatWindowStopBlinking', self)
    693 
    694     def GetMenu(self, *args):
    695         menu = MenuData()
    696         menu.AddCaption(self.caption)
    697         menu.AddSeparator()
    698         self._add_channel_menu(menu)
    699         tab_menu = CreateMenuDataFromRawTuples(super(ChatWindow, self).GetMenu(*args))
    700         if tab_menu is not None and tab_menu.GetEntries():
    701             menu += tab_menu
    702             menu.AddSeparator()
    703         return menu
    704 
    705     def GetMenuMoreOptions(self):
    706         menu = super(ChatWindow, self).GetMenuMoreOptions()
    707         self._add_channel_menu(menu)
    708         return menu
    709 
    710     def _add_channel_menu(self, menu):
    711         gm_options = MenuData()
    712         if session.role & ROLE_GML:
    713             gm_options.AddEntry(text='channel id: ' + self._controller.channel_id, func=lambda : blue.pyos.SetClipboardData(self._controller.channel_id))
    714             gm_options.AddEntry(text='category id: ' + self._controller.category_id, func=lambda : blue.pyos.SetClipboardData(self._controller.category_id))
    715             gm_options.AddEntry(text='window id: ' + self._controller.window_id, func=lambda : blue.pyos.SetClipboardData(self._controller.window_id))
    716             gm_options.AddSeparator()
    717             gm_options.AddEntry(text='Reload Members', func=self._reload_members)
    718             gm_options.AddEntry(text='Reload Messages', func=self._reload_messages)
    719             gm_options.AddEntry(text='Copy Members Data', func=self._qa_copy_member_data)
    720         if gm_options.GetEntries():
    721             menu.AddEntry(text='GM / WM Extras', subMenuData=gm_options)
    722         elevated_role_mask = ROLE_CENTURION | ROLE_LEGIONEER | ROLEMASK_ELEVATEDPLAYER
    723         if session.role & elevated_role_mask:
    724             menu.AddCheckbox(text='Special chat highlighting', setting=elevated_chat_highlighting_setting)
    725             menu.AddSeparator()
    726         menu.AddLabel(localization.GetByLabel('UI/Chat/ChannelSettingsSection'))
    727         member_list_options = MenuData()
    728         member_list_options.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowMemberList'), setting=self._show_member_list_setting)
    729         member_list_options.AddSeparator()
    730         member_list_options.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowCompactMemberList'), setting=self._compact_member_entries_setting)
    731         menu.AddEntry(text=localization.GetByLabel('UI/Chat/MemberList'), texturePath=eveicon.people, subMenuData=member_list_options)
    732         font_size_menu = MenuData()
    733         font_size_menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/UseFontSizeInAllChannels'), setting=self._apply_font_size_globally_setting)
    734         font_size_menu.AddSeparator()
    735         for font_size in self._effective_font_size_setting.options:
    736             font_size_menu.AddRadioButton(text=str(font_size), value=font_size, setting=self._effective_font_size_setting)
    737 
    738         menu.AddEntry(text=localization.GetByLabel('/Carbon/UI/Controls/EditRichText/FontSize'), texturePath=eveicon.font_size, subMenuData=font_size_menu)
    739         message_style_menu = MenuData()
    740         for mode in self._message_style_setting.options:
    741             message_style_menu.AddRadioButton(text=localization.GetByLabel(CHAT_MESSAGE_MODE_LABEL_PATH[mode]), value=mode, setting=self._message_style_setting)
    742 
    743         menu.AddEntry(text=localization.GetByLabel('UI/Chat/MessagePortraits'), texturePath=eveicon.person, subMenuData=message_style_menu)
    744         if has_hide_message_setting(self._controller.category_id):
    745             menu.AddEntry(text=localization.GetByLabel('UI/Chat/MessageFilterContextMenu'), texturePath=eveicon.filter, func=self._open_hide_message_settings)
    746         menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/BlinkOnNewActivity'), setting=self._chat_notifications_enabled_setting)
    747         if self._controller.motd:
    748             menu.AddEntry(text=localization.GetByLabel('UI/Chat/ReloadChannelMOTD'), func=self._show_motd)
    749         menu.AddEntry(text=localization.GetByLabel('UI/Chat/ClearAllContent'), func=self._controller.clear_messages)
    750         menu.AddSeparator()
    751         menu.AddLabel(localization.GetByLabel('UI/Chat/GlobalSettingsSection'))
    752         autoCollapseMenu = MenuData()
    753         autoCollapseMenu.AddCheckbox(localization.GetByLabel('UI/Chat/AutoCollapseMessagesEnabled'), hint=localization.GetByLabel('UI/Chat/AutoCollapseMessagesHint'), setting=auto_collapse_messages)
    754         autoCollapseMenu.AddSlider(localization.GetByLabel('UI/Chat/AutoCollapseMessagesLines'), setting=auto_collapse_message_lines, isInteger=True)
    755         menu.AddEntry(text=localization.GetByLabel('UI/Chat/AutoCollapseMessages'), hint=localization.GetByLabel('UI/Chat/AutoCollapseMessagesHint'), texturePath=eveicon.collapse, subMenuData=autoCollapseMenu)
    756         menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/HighlightMyMessages'), setting=highlight_my_messages_setting)
    757         menu.AddCheckbox(text=localization.GetByLabel('UI/Chat/ShowTimestamp'), setting=show_message_timestamp_setting)
    758         menu.AddEntry(text=localization.GetByLabel('UI/Chat/ConfigureWordFiltersAndHighlights'), func=self._set_word_and_highlight_filters)
    759         return menu
    760 
    761     def _set_word_and_highlight_filters(self):
    762         ChatFilterSettings.Open()
    763 
    764     def _open_hide_message_settings(self):
    765         self._controller.open_hide_message_settings()
    766 
    767     def _show_motd(self):
    768         motd = self._controller.motd
    769         if motd:
    770             message = localization.GetByLabel('UI/Chat/ChannelMotd', motd=motd)
    771             self._controller.add_game_message(text=message, sender_id=ownerSystem)
    772 
    773     def _qa_copy_member_data(self):
    774         import json
    775         data = {member_id:self._controller.get_member_data(member_id) for member_id in self._controller.member_ids}
    776         blue.pyos.SetClipboardData(json.dumps(data, indent=2, sort_keys=True))
    777 
    778 
    779 def GetIdxForUser_WithBinarySearch(member_list_scroll, entry_name):
    780     a = member_list_scroll.GetNodes()
    781     x = entry_name
    782     lo = 0
    783     hi = len(a)
    784     while lo < hi:
    785         mid = (lo + hi) // 2
    786         if x < a[mid].charIndex:
    787             hi = mid
    788         else:
    789             lo = mid + 1
    790 
    791     return lo