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

jumpClonesPanel.py (47382B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\shared\neocom\charsheet\jumpClonesPanel.py
      2 import contextlib
      3 import datetime
      4 import functools
      5 import logging
      6 import math
      7 import random
      8 import caching
      9 import datetimeutils
     10 import dogma.const
     11 import enum
     12 import eveformat.client
     13 import eveicon
     14 import evelink.client
     15 import evetypes
     16 import eveui
     17 import inventorycommon.const
     18 import localization
     19 import signals
     20 import threadutils
     21 import trinity
     22 import uthread2
     23 from carbonui import ButtonStyle, ButtonVariant, TextColor, uiconst
     24 from carbonui.control import scrollContainer
     25 from carbonui.control.button import Button
     26 from carbonui.primitives import container, containerAutoSize, layoutGrid, sprite
     27 from carbonui.primitives.containerAutoSize import ContainerAutoSize
     28 from carbonui.uianimations import animations
     29 from eve.client.script.ui import eveColor, eveThemeColor
     30 from eve.client.script.ui.control import eveLabel
     31 from eve.client.script.ui.control import eveLoadingWheel
     32 from eve.client.script.ui.control import infoIcon
     33 from eve.client.script.ui.control import itemIcon
     34 from eve.client.script.ui.control import themeColored
     35 from eve.client.script.ui.control import utilMenu
     36 from carbonui.button.group import ButtonGroup
     37 from eve.client.script.ui.shared.shipTree import infoBubble
     38 from eve.client.script.ui.tooltips import tooltipUtil
     39 from eve.client.script.ui.util import utilWindows
     40 from eve.common.script.sys.idCheckers import IsStation
     41 from eve.common.script.util.eveFormat import FmtISK
     42 from eveservices.menu import GetMenuService
     43 from inventorycommon.typeHelpers import GetAveragePrice
     44 from menucheckers.sessionChecker import SessionChecker
     45 log = logging.getLogger(__name__)
     46 CLONE_LIMIT_SKILLS = [inventorycommon.const.typeInfomorphPsychology, inventorycommon.const.typeAdvancedInfomorphPsychology, inventorycommon.const.typeEliteInfomorphPsychology]
     47 CLONE_JUMP_COOLDOWN_SKILLS = [inventorycommon.const.typeInfomorphSynchronizing]
     48 JUMP_CLONE_SKILLS = CLONE_LIMIT_SKILLS + CLONE_JUMP_COOLDOWN_SKILLS
     49 COLOR_WARNING = eveColor.WARNING_ORANGE
     50 COLOR_TEXT_SECONDARY = TextColor.SECONDARY
     51 
     52 def jump_clone_label_path(name):
     53     return 'UI/CharacterSheet/CharacterSheetWindow/JumpCloneScroll/{}'.format(name)
     54 
     55 
     56 def jump_clone_label(name, **kwargs):
     57     return localization.GetByLabel(jump_clone_label_path(name), **kwargs)
     58 
     59 
     60 class OptionEnum(enum.Enum):
     61 
     62     def __new__(cls, value, label):
     63         obj = object.__new__(cls)
     64         obj._value_ = value
     65         obj._label = label
     66         return obj
     67 
     68     @property
     69     def label(self):
     70         return localization.GetByLabel(self._label)
     71 
     72     @classmethod
     73     def __reload_update__(cls, oldobj):
     74         return cls
     75 
     76 
     77 class FilterBy(OptionEnum):
     78     all = (1, jump_clone_label_path('FilterByAll'))
     79     station = (2, jump_clone_label_path('FilterByStation'))
     80     structure = (3, jump_clone_label_path('FilterByStructure'))
     81     ship = (4, jump_clone_label_path('FilterByShip'))
     82 
     83 
     84 class ArrangeBy(OptionEnum):
     85     location = (1, jump_clone_label_path('ArrangeByLocation'))
     86     name = (2, jump_clone_label_path('ArrangeByName'))
     87     implant_count = (3, jump_clone_label_path('ArrangeByImplantCount'))
     88 
     89 
     90 class SortOrder(OptionEnum):
     91     ascending = (1, jump_clone_label_path('SortOrderAscending'))
     92     descending = (2, jump_clone_label_path('SortOrderDescending'))
     93 
     94 
     95 class JumpClonesPanel(container.Container):
     96     default_name = 'JumpClonesPanel'
     97 
     98     def __init__(self, **kwargs):
     99         self.controller = JumpClonePanelController()
    100         self.clone_entries = []
    101         self.is_refreshing = False
    102         self.needs_refresh = False
    103         self.jump_cooldown_banner = None
    104         self.install_clone_banner = None
    105         self.no_clones_message = None
    106         super(JumpClonesPanel, self).__init__(**kwargs)
    107         self.layout()
    108         self.controller.on_update.connect(self.refresh)
    109         self.controller.on_filter_change.connect(self.on_filter_change)
    110 
    111     def on_filter_change(self):
    112         self.refresh_clone_list()
    113 
    114     def layout(self):
    115         self.loading_overlay = container.Container(parent=self, align=uiconst.TOALL, state=uiconst.UI_NORMAL, bgColor=(0.0, 0.0, 0.0), opacity=0.5)
    116         self.main_cont = container.Container(parent=self, align=uiconst.TOALL)
    117         self.loading_wheel = eveLoadingWheel.LoadingWheel(parent=self.loading_overlay, align=uiconst.CENTER, state=uiconst.UI_DISABLED)
    118         self.error_message = ErrorMessage(parent=self.loading_overlay, align=uiconst.CENTER)
    119         self.footer = container.Container(parent=self.main_cont, align=uiconst.TOBOTTOM, height=32)
    120         self.clone_limit_label = eveLabel.EveLabelMedium(parent=self.footer, align=uiconst.CENTERLEFT, state=uiconst.UI_NORMAL, left=12)
    121         self.clone_limit_label.LoadTooltipPanel = load_clone_limit_tootltip
    122         filter_cont = containerAutoSize.ContainerAutoSize(parent=self.main_cont, align=uiconst.TOTOP)
    123         self.filter_menu = FilterMenu(parent=filter_cont, align=uiconst.TOPLEFT, controller=self.controller)
    124         self.clone_scroll = scrollContainer.ScrollContainer(parent=self.main_cont, align=uiconst.TOALL, alignMode=uiconst.TOTOP, padding=(0, 4, 0, 0))
    125 
    126     def Close(self):
    127         self.controller.close()
    128         super(JumpClonesPanel, self).Close()
    129 
    130     def LoadPanel(self, *args):
    131         self.refresh()
    132 
    133     def refresh(self):
    134         if not self.display:
    135             return
    136         if not session.charid:
    137             return
    138         if self.is_refreshing:
    139             self.needs_refresh = True
    140             return
    141         self.is_refreshing = True
    142         self.needs_refresh = False
    143         try:
    144             with self.loading():
    145                 with self.keep_scroll_position():
    146                     self.controller.pre_load()
    147                     self.clear_clone_list()
    148                     if not self.needs_refresh:
    149                         self.load_content()
    150         finally:
    151             self.is_refreshing = False
    152 
    153         if self.needs_refresh:
    154             uthread2.start_tasklet(self.refresh)
    155 
    156     def refresh_clone_list(self):
    157         if not self.display:
    158             return
    159         with self.keep_scroll_position():
    160             self.clear_clone_list()
    161             self.load_clone_list()
    162 
    163     def clear_clone_list(self):
    164         if self.no_clones_message:
    165             self.no_clones_message.Close()
    166             self.no_clones_message = None
    167         entries = self.clone_entries
    168         self.clone_entries = []
    169         for entry in entries:
    170             entry.Close()
    171 
    172     @contextlib.contextmanager
    173     def loading(self):
    174         self.error_message.Hide()
    175         self.loading_overlay.Enable()
    176         animations.FadeIn(self.loading_overlay, endVal=0.5, duration=0.5)
    177         animations.FadeIn(self.loading_wheel, duration=0.3)
    178         try:
    179             yield
    180         except Exception:
    181             animations.FadeOut(self.main_cont, duration=0.3)
    182             animations.FadeOut(self.loading_wheel, duration=0.1)
    183             self.error_message.Show()
    184             self.error_message.SetText(jump_clone_label('UnknownErrorMessage'))
    185             log.exception('Failed to load the jump clones panel in the character sheet')
    186         else:
    187             self.loading_overlay.Disable()
    188             animations.FadeOut(self.loading_overlay, duration=0.3)
    189             animations.FadeIn(self.main_cont, duration=0.3)
    190 
    191     @contextlib.contextmanager
    192     def keep_scroll_position(self):
    193         self.clone_scroll.mainCont.DisableAutoSize()
    194         try:
    195             yield
    196         finally:
    197             eveui.call_next_frame(self.clone_scroll.mainCont.EnableAutoSize)
    198 
    199     def load_content(self):
    200         if self.controller.clone_limit == 0:
    201             text = localization.GetByLabel('UI/Medical/JumpCloneSkillReqNotMet')
    202             color = COLOR_WARNING
    203         else:
    204             text = localization.GetByLabel('UI/Medical/JumpCloneUsageAndCapacity', count=len(self.controller.clones), limit=self.controller.clone_limit)
    205             if self.controller.clone_count == self.controller.clone_limit:
    206                 color = COLOR_WARNING
    207             else:
    208                 color = self.clone_limit_label.default_color
    209         self.clone_limit_label.SetRGBA(*color)
    210         self.clone_limit_label.SetText(text)
    211         if not self.jump_cooldown_banner:
    212             self.jump_cooldown_banner = JumpCooldownBanner(parent=self.clone_scroll, align=uiconst.TOTOP, controller=self.controller)
    213         if not self.install_clone_banner:
    214             self.install_clone_banner = InstallCloneBanner(parent=self.clone_scroll, align=uiconst.TOTOP, padTop=4, controller=self.controller)
    215         self.load_clone_list()
    216 
    217     def load_clone_list(self):
    218         clones = self.controller.clones_filtered_and_sorted
    219         for i, clone in enumerate(clones):
    220             entry = JumpCloneEntry(parent=self.clone_scroll, align=uiconst.TOTOP, padTop=4 if i > 0 else 8, controller=self.controller, clone=clone, jump_errors=self.controller.validate_jump_to(clone), on_expand=self.on_clone_entry_expand)
    221             self.clone_entries.append(entry)
    222             animations.FadeTo(entry, startVal=0.0, endVal=1.0, duration=0.25, timeOffset=0.02 * i)
    223 
    224         if not clones and not self.controller.clones:
    225             self.display_no_clones_message()
    226         elif not clones:
    227             self.display_no_clones_match_filter_message()
    228 
    229     @threadutils.threaded
    230     def on_clone_entry_expand(self, jump_clone_entry):
    231         eveui.wait_for_next_frame()
    232         self.clone_scroll.ScrollToRevealChildVertical(jump_clone_entry)
    233 
    234     def display_no_clones_message(self):
    235         self.no_clones_message = containerAutoSize.ContainerAutoSize(parent=self.clone_scroll, align=uiconst.TOTOP, top=64, padding=(64, 0, 64, 0))
    236         icon_cont = container.Container(parent=self.no_clones_message, align=uiconst.TOTOP, height=64, width=72)
    237         sprite.Sprite(parent=icon_cont, align=uiconst.CENTER, height=64, width=64, texturePath='res:/UI/Texture/WindowIcons/jumpclones.png', opacity=0.25)
    238         infoIcon.MoreInfoIcon(parent=icon_cont, align=uiconst.CENTERTOP, left=42, hint=localization.GetByLabel('UI/Medical/JumpCloneDescription'))
    239         eveLabel.EveCaptionMedium(parent=self.no_clones_message, align=uiconst.TOTOP, text=eveformat.center(jump_clone_label('NoJumpClonesFound')), opacity=0.5)
    240         eveLabel.EveLabelMedium(parent=self.no_clones_message, align=uiconst.TOTOP, text=eveformat.center(jump_clone_label('NoJumpClonesFoundDescription')), opacity=0.25)
    241 
    242     def display_no_clones_match_filter_message(self):
    243         self.no_clones_message = containerAutoSize.ContainerAutoSize(parent=self.clone_scroll, align=uiconst.TOTOP, top=64, padding=(64, 0, 64, 0))
    244         eveLabel.EveCaptionMedium(parent=self.no_clones_message, align=uiconst.TOTOP, text=eveformat.center(jump_clone_label('NoJumpClonesMatchFilter')), opacity=0.5)
    245         eveLabel.EveLabelMedium(parent=self.no_clones_message, align=uiconst.TOTOP, text=eveformat.center(jump_clone_label('NoJumpClonesMatchFilterDescription')), opacity=0.25)
    246 
    247 
    248 def load_clone_limit_tootltip(panel, parent):
    249     panel.state = uiconst.UI_NORMAL
    250     panel.LoadGeneric2ColumnTemplate()
    251     panel.margin = (8, 8, 8, 8)
    252     for type_id in CLONE_LIMIT_SKILLS:
    253         panel.AddRow(rowClass=infoBubble.SkillEntry, typeID=type_id, level=0, showLevel=False, skillBarPadding=(24, 8, 0, 8))
    254 
    255 
    256 class FilterMenu(utilMenu.UtilMenu):
    257     default_texturePath = eveicon.tune
    258     default_iconSize = 16
    259     default_labelAlign = uiconst.CENTERRIGHT
    260 
    261     def __init__(self, controller, **kwargs):
    262         self.controller = controller
    263         kwargs['GetUtilMenu'] = self.get_util_menu
    264         super(FilterMenu, self).__init__(**kwargs)
    265         self.update_label()
    266 
    267     def on_filter_by_change(self, filter_by):
    268         self.controller.filter_by = filter_by
    269         self.update_label()
    270 
    271     def on_arrange_by_change(self, arrange_by):
    272         self.controller.arrange_by = arrange_by
    273         self.update_label()
    274 
    275     def on_sort_order_change(self, sort_order):
    276         self.controller.sort_order = sort_order
    277         self.update_label()
    278 
    279     def get_util_menu(self, menu):
    280         menu.AddText(jump_clone_label('FilterHeader'))
    281         for filter_by in FilterBy:
    282             menu.AddRadioButton(filter_by.label, checked=self.controller.filter_by == filter_by, callback=functools.partial(self.on_filter_by_change, filter_by=filter_by))
    283 
    284         menu.AddSpace(height=12)
    285         menu.AddText(jump_clone_label('ArrangeByHeader'))
    286         for arrange_by in ArrangeBy:
    287             menu.AddRadioButton(arrange_by.label, checked=self.controller.arrange_by == arrange_by, callback=functools.partial(self.on_arrange_by_change, arrange_by=arrange_by))
    288 
    289         menu.AddSpace(height=12)
    290         menu.AddText(jump_clone_label('SortHeader'))
    291         for sort_order in SortOrder:
    292             menu.AddRadioButton(sort_order.label, checked=self.controller.sort_order == sort_order, callback=functools.partial(self.on_sort_order_change, sort_order=sort_order))
    293 
    294     def update_label(self):
    295         text = u'{}; {}'.format(self.controller.filter_by.label, self.controller.arrange_by.label)
    296         self.SetLabel(text)
    297 
    298 
    299 class SettingsProperty(object):
    300 
    301     def __init__(self, key, default = None, deserialize = None, serialize = None, on_change = None):
    302         parts = key.split('.')
    303         if len(parts) > 1:
    304             group = parts[:-1]
    305             key = parts[-1]
    306         else:
    307             raise ValueError('You must have the settings group in the key, f.ex. "char.ui.some_key"')
    308         self.key = key
    309         self.default = default
    310         self._group = group
    311         self._deserialize = None
    312         self._serialize = None
    313         self._on_change = None
    314         self._settings = None
    315         if deserialize:
    316             self._deserialize = lambda obj, value: deserialize(value)
    317         if serialize:
    318             self._serialize = lambda obj, value: serialize(value)
    319         if on_change:
    320             self._on_change = lambda obj: on_change()
    321 
    322     @property
    323     def deserialize(self):
    324 
    325         def deco(func):
    326             self._deserialize = func
    327             return self
    328 
    329         return deco
    330 
    331     @property
    332     def serialize(self):
    333 
    334         def deco(func):
    335             self._setter = func
    336             return self
    337 
    338         return deco
    339 
    340     @property
    341     def on_change(self):
    342 
    343         def deco(func):
    344             self._on_change = func
    345             return self
    346 
    347         return deco
    348 
    349     @property
    350     def settings(self):
    351         if self._settings is None:
    352             import __builtin__
    353             settings = __builtin__.settings
    354             for group in self._group:
    355                 settings = getattr(settings, group)
    356 
    357             self._settings = settings
    358         return self._settings
    359 
    360     def __get__(self, obj, obj_type = None):
    361         if obj is None:
    362             return self
    363         value = self.settings.Get(self.key, self.default)
    364         if self._deserialize:
    365             value = self._deserialize(obj, value)
    366         return value
    367 
    368     def __set__(self, obj, value):
    369         current = self.__get__(obj)
    370         if value == current:
    371             return
    372         if self._serialize:
    373             value = self._serialize(obj, value)
    374         self.settings.Set(self.key, value)
    375         if self._on_change:
    376             self._on_change(obj)
    377 
    378     def __delete__(self, obj):
    379         self.settings.Delete(self.key)
    380 
    381 
    382 def enum_settings_property(enum, key, default):
    383     return SettingsProperty(key=key, default=default, serialize=lambda e: e.value, deserialize=enum)
    384 
    385 
    386 class JumpClonePanelController(object):
    387     __notifyevents__ = ['OnCloneJumpUpdate', 'OnSessionChanged', 'OnSkillsChanged']
    388 
    389     def __init__(self):
    390         self.on_update = signals.Signal(signalName='on_update')
    391         self.on_filter_change = signals.Signal(signalName='on_filter_change')
    392         self.on_expand = signals.Signal(signalName='on_expand')
    393         self.on_collapse = signals.Signal(signalName='on_collapse')
    394         sm.RegisterNotify(self)
    395         if not self.is_jump_available:
    396             self._jump_cooldown_thread()
    397 
    398     @property
    399     def clones(self):
    400         clones = []
    401         for clone_info in self._clone_jump_service.GetClones():
    402             implants = self._clone_jump_service.GetImplantsForClone(clone_info.jumpCloneID)
    403             clone = JumpClone(clone_id=clone_info.jumpCloneID, location_id=clone_info.locationID, name=clone_info.cloneName, implants=[ implant.typeID for implant in implants or [] ])
    404             clones.append(clone)
    405 
    406         return clones
    407 
    408     @property
    409     def clones_filtered_and_sorted(self):
    410 
    411         def filter_criteria(clone):
    412             if self.filter_by == FilterBy.all:
    413                 return True
    414             elif self.filter_by == FilterBy.station:
    415                 return clone.in_station
    416             elif self.filter_by == FilterBy.structure:
    417                 return clone.in_structure
    418             elif self.filter_by == FilterBy.ship:
    419                 return clone.in_ship
    420             else:
    421                 return False
    422 
    423         def sort_key(clone):
    424             if self.arrange_by == ArrangeBy.location:
    425                 primary_key = clone.location_name.lower()
    426                 secondary_key = clone.name
    427             elif self.arrange_by == ArrangeBy.name:
    428                 primary_key = clone.name.lower()
    429                 secondary_key = clone.location_name
    430             elif self.arrange_by == ArrangeBy.implant_count:
    431                 primary_key = len(clone.implants)
    432                 secondary_key = clone.location_name
    433             current_location_key = not clone.in_current_location if self.sort_order == SortOrder.ascending else clone.in_current_location
    434             return (current_location_key, primary_key, secondary_key.lower())
    435 
    436         return sorted(filter(filter_criteria, self.clones), key=sort_key, reverse=self.sort_order == SortOrder.descending)
    437 
    438     @property
    439     def clone_count(self):
    440         return len(self.clones)
    441 
    442     @property
    443     def clone_limit(self):
    444         dogma_location = sm.GetService('clientDogmaIM').GetDogmaLocation()
    445         dogma_location.WaitForShip()
    446         return int(dogma_location.GetAttributeValue(session.charid, dogma.const.attributeMaxJumpClones))
    447 
    448     @property
    449     def is_jump_available(self):
    450         return self.remaining_time_until_next_jump.total_seconds() <= 0
    451 
    452     @property
    453     def last_jump_timestamp(self):
    454         last_jump_filetime = self._clone_jump_service.LastCloneJumpTime()
    455         if last_jump_filetime:
    456             return datetimeutils.filetime_to_datetime(last_jump_filetime)
    457 
    458     @property
    459     def next_jump_timestamp(self):
    460         if self.last_jump_timestamp:
    461             return self.last_jump_timestamp + self.jump_cooldown_duration
    462 
    463     @property
    464     def remaining_time_until_next_jump(self):
    465         if self.next_jump_timestamp is None:
    466             return datetime.timedelta()
    467         now = datetime.datetime.utcnow()
    468         if self.next_jump_timestamp < now:
    469             return datetime.timedelta()
    470         return self.next_jump_timestamp - now
    471 
    472     @property
    473     def jump_cooldown_duration(self):
    474         character = sm.GetService('godma').GetStateManager().GetItem(session.charid, waitForPrime=True)
    475         return datetime.timedelta(hours=int(round(character.cloneJumpCoolDown)))
    476 
    477     @property
    478     def clone_count_installed_here(self):
    479         return len(filter(lambda clone: clone.location_id in (session.stationid, session.structureid), self.clones))
    480 
    481     @property
    482     def is_clone_installed_here(self):
    483         return self.clone_count_installed_here > 0
    484 
    485     @property
    486     def is_docked(self):
    487         return SessionChecker(session, sm).IsPilotDocked()
    488 
    489     @property
    490     def location_id(self):
    491         return session.structureid or session.stationid
    492 
    493     @property
    494     def _clone_jump_service(self):
    495         return sm.GetService('clonejump')
    496 
    497     filter_by = enum_settings_property(FilterBy, key='char.ui.jump_clones_panel_filter_by', default=FilterBy.all)
    498 
    499     @filter_by.on_change
    500     def filter_by(self):
    501         self.on_filter_change()
    502 
    503     arrange_by = enum_settings_property(ArrangeBy, key='char.ui.jump_clones_panel_arrange_by', default=ArrangeBy.location)
    504 
    505     @arrange_by.on_change
    506     def arrange_by(self):
    507         self.on_filter_change()
    508 
    509     sort_order = enum_settings_property(SortOrder, key='char.ui.jump_clones_panel_sort_order', default=SortOrder.ascending)
    510 
    511     @sort_order.on_change
    512     def sort_order(self):
    513         self.on_filter_change()
    514 
    515     expanded_clone_ids = SettingsProperty(key='char.ui.jump_clones_panel_expanded_clone_ids', default=None, deserialize=lambda value: (tuple() if value is None else value), serialize=lambda value: tuple(value))
    516 
    517     def is_expanded(self, clone_id):
    518         return clone_id in self.expanded_clone_ids
    519 
    520     def expand(self, clone_id):
    521         existing_clone_ids = [ clone.id for clone in self.clones ]
    522         expanded_clone_ids = set((cid for cid in self.expanded_clone_ids if cid in existing_clone_ids))
    523         expanded_clone_ids.add(clone_id)
    524         self.expanded_clone_ids = tuple(expanded_clone_ids)
    525         self.on_expand(clone_id)
    526 
    527     def collapse(self, clone_id):
    528         existing_clone_ids = [ clone.id for clone in self.clones ]
    529         self.expanded_clone_ids = tuple((cid for cid in self.expanded_clone_ids if cid != clone_id and cid in existing_clone_ids))
    530         self.on_collapse(clone_id)
    531 
    532     def toggle_expanded(self, clone_id):
    533         if self.is_expanded(clone_id):
    534             self.collapse(clone_id)
    535         else:
    536             self.expand(clone_id)
    537 
    538     def pre_load(self):
    539         clones = self.clones
    540         cfg.evelocations.Prime([ clone.location_id for clone in clones ])
    541         self.clone_limit
    542         self.is_jump_available
    543 
    544     def close(self):
    545         sm.UnregisterNotify(self)
    546         self._jump_cooldown_thread.kill_thread_maybe()
    547 
    548     def can_jump_to(self, clone):
    549         return len(self.validate_jump_to(clone)) == 0
    550 
    551     def jump(self, clone):
    552         self._clone_jump_service.CloneJump(clone.location_id, clone.id)
    553 
    554     def destroy(self, clone):
    555         self._clone_jump_service.DestroyInstalledClone(clone.id)
    556 
    557     def rename(self, clone):
    558         new_name = utilWindows.NamePopup(localization.GetByLabel('UI/Menusvc/SetName'), localization.GetByLabel('UI/Menusvc/TypeInNewName'), setvalue=clone.name, maxLength=100)
    559         if new_name:
    560             self._clone_jump_service.SetJumpCloneName(clone.id, new_name)
    561 
    562     def install(self):
    563         self._clone_jump_service.InstallCloneInStation()
    564 
    565     def validate_jump_to(self, clone):
    566         errors = []
    567         checker = SessionChecker(session, sm)
    568         should_check_timer = not clone.in_current_location or clone.in_ship
    569         if should_check_timer and not self.is_jump_available:
    570             errors.append(jump_clone_label('JumpCooldownError'))
    571         if not checker.IsPilotDocked() or checker.IsPilotControllingStructure():
    572             errors.append(jump_clone_label('JumpNotDockedError'))
    573         if self._clone_jump_service.IsCloneJumping():
    574             if self._clone_jump_service.IsJumpingIntoClone(clone.id):
    575                 errors.append(jump_clone_label('CurrentlyJumpingToThisCloneError'))
    576             else:
    577                 errors.append(jump_clone_label('CurrentlyJumpingToAnotherError'))
    578         return errors
    579 
    580     def validate_install_clone(self):
    581         checker = SessionChecker(session, sm)
    582         if not checker.IsPilotDocked():
    583             return [jump_clone_label('JumpNotDockedError')]
    584         return self._clone_jump_service.ValidateInstallJumpClone()
    585 
    586     def get_jump_label(self, cloneID):
    587         if self._clone_jump_service.IsJumpingIntoClone(cloneID):
    588             return jump_clone_label('Jumping')
    589         return jump_clone_label('Jump')
    590 
    591     @threadutils.highlander_threaded
    592     def _jump_cooldown_thread(self):
    593         while not self.is_jump_available:
    594             uthread2.sleep(max(1.0, self.remaining_time_until_next_jump.total_seconds()))
    595 
    596         self.on_update()
    597 
    598     def OnCloneJumpUpdate(self):
    599         uthread2.start_tasklet(self.on_update)
    600 
    601     def OnSessionChanged(self, isRemote, sess, change):
    602         uthread2.start_tasklet(self.on_update)
    603 
    604     def OnSkillsChanged(self, skillInfos):
    605         for type_id, skill in skillInfos.iteritems():
    606             if type_id in JUMP_CLONE_SKILLS:
    607                 uthread2.start_tasklet(self.on_update)
    608                 return
    609 
    610 
    611 class JumpClone(object):
    612 
    613     def __init__(self, clone_id, location_id, name, implants):
    614         self.id = clone_id
    615         self.location_id = location_id
    616         self._name = name
    617         self.implants = implants
    618 
    619     @property
    620     def name(self):
    621         return self._name or self.default_name
    622 
    623     @name.setter
    624     def name(self, name):
    625         self._name = name
    626 
    627     @caching.lazy_property
    628     def default_name(self):
    629         alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    630         generator = random.Random(self.id)
    631         identity = u''.join([ generator.choice(alphabet) for x in range(4) ])
    632         return jump_clone_label('DefaultCloneName', identity=identity)
    633 
    634     @caching.lazy_property
    635     def location_name(self):
    636         try:
    637             return cfg.evelocations.Get(self.location_id).name
    638         except KeyError:
    639             return jump_clone_label('DestroyedLocation')
    640 
    641     @caching.lazy_property
    642     def has_implants(self):
    643         return len(self.implants) > 0
    644 
    645     @caching.lazy_property
    646     def in_station(self):
    647         return IsStation(self.location_id)
    648 
    649     @caching.lazy_property
    650     def in_structure(self):
    651         return sm.GetService('structureDirectory').IsStructure(self.location_id)
    652 
    653     @caching.lazy_property
    654     def in_ship(self):
    655         return not (self.in_station or self.in_structure)
    656 
    657     @property
    658     def in_current_location(self):
    659         return self.location_id in (session.stationid, session.structureid)
    660 
    661 
    662 class ErrorMessage(containerAutoSize.ContainerAutoSize):
    663 
    664     def __init__(self, **kwargs):
    665         kwargs.setdefault('width', 300)
    666         super(ErrorMessage, self).__init__(**kwargs)
    667         self.icon = sprite.Sprite(parent=container.Container(parent=self, align=uiconst.TOTOP, height=64), align=uiconst.CENTER, state=uiconst.UI_DISABLED, texturePath='res:/UI/Texture/WindowIcons/aggression.png', width=64, height=64)
    668         eveLabel.EveCaptionMedium(parent=self, align=uiconst.TOTOP, text=eveformat.center(jump_clone_label('UnknownErrorTitle')))
    669         self.label = eveLabel.EveLabelMedium(parent=self, align=uiconst.TOTOP)
    670 
    671     def SetText(self, text):
    672         self.label.SetText(eveformat.center(text))
    673 
    674 
    675 class JumpCloneEntry(containerAutoSize.ContainerAutoSize):
    676     default_minHeight = 64
    677     default_alignMode = uiconst.TOTOP
    678 
    679     def __init__(self, controller, clone, jump_errors, on_expand = None, **kwargs):
    680         super(JumpCloneEntry, self).__init__(**kwargs)
    681         self.controller = controller
    682         self.clone = clone
    683         self.jump_errors = jump_errors
    684         self.implant_list = None
    685         self.on_expand_anim_finished = signals.Signal(signalName='on_expand_anim_finished')
    686         self.layout()
    687         if on_expand is not None:
    688             self.on_expand_anim_finished.connect(on_expand)
    689         self.controller.on_expand.connect(self.on_expand)
    690         self.controller.on_collapse.connect(self.on_collapse)
    691 
    692     def layout(self):
    693         if self.clone.in_current_location:
    694             in_current_location_cont = containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TOTOP)
    695             themeColored.FillThemeColored(bgParent=in_current_location_cont, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.1)
    696             eveLabel.EveLabelMediumBold(parent=in_current_location_cont, align=uiconst.TOPLEFT, padding=(8, 4, 8, 4), text=jump_clone_label('InCurrentStation'))
    697             themeColored.FillThemeColored(bgParent=self, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.1)
    698         else:
    699             themeColored.FillThemeColored(bgParent=self, colorType=uiconst.COLORTYPE_UIBASECONTRAST)
    700         jump_clone_icon = JumpCloneIcon(parent=container.Container(parent=self, align=uiconst.TOLEFT, width=64, padRight=8), align=uiconst.CENTERTOP, top=4, clone=self.clone, state=uiconst.UI_NORMAL)
    701         top_cont = containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TOTOP, alignMode=uiconst.TOTOP)
    702         top_right_cont = containerAutoSize.ContainerAutoSize(parent=top_cont, align=uiconst.TORIGHT)
    703         eveLabel.EveLabelLarge(parent=top_cont, align=uiconst.TOTOP, padTop=4, text=self.clone.name)
    704         if self.clone.in_ship:
    705             location_text = self.clone.location_name
    706         else:
    707             location_text = evelink.location_link(self.clone.location_id)
    708         location_text = jump_clone_label('CloneLocationPrefix', location=location_text)
    709         eveLabel.EveLabelMedium(parent=top_cont, align=uiconst.TOTOP, state=uiconst.UI_NORMAL, text=location_text)
    710         if len(self.clone.implants) == 0:
    711             eveLabel.EveLabelMedium(parent=self, align=uiconst.TOTOP, padding=(0, 8, 0, 8), text=jump_clone_label('NoImplantsInstalled'), color=TextColor.DISABLED)
    712         else:
    713             ImplantsOverview(parent=containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TOTOP, padding=(0, 8, 0, 8)), align=uiconst.TOPLEFT, controller=self.controller, clone=self.clone)
    714         button_cont = ButtonGroup(parent=top_right_cont, align=uiconst.TOPLEFT, padding=8)
    715         jump_button = Button(parent=button_cont, label=self.controller.get_jump_label(self.clone.id), func=lambda button: self.controller.jump(self.clone))
    716         if self.jump_errors:
    717             jump_button.Disable()
    718             jump_button.LoadTooltipPanel = functools.partial(load_errors_tooltip, errors=self.jump_errors)
    719         Button(parent=button_cont, label=jump_clone_label('Rename'), variant=ButtonVariant.GHOST, func=lambda button: self.controller.rename(self.clone))
    720         Button(parent=button_cont, label=jump_clone_label('Destroy'), style=ButtonStyle.DANGER, variant=ButtonVariant.GHOST, func=lambda button: self.controller.destroy(self.clone))
    721         self.set_clone_value_hint(jump_clone_icon)
    722         if self.controller.is_expanded(self.clone.id):
    723             self.create_implant_list()
    724 
    725     def create_implant_list(self):
    726         self.implant_list = containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TOTOP, padBottom=8, clipChildren=True)
    727         self.implant_list.DisableAutoSize()
    728         for type_id in sorted(self.clone.implants, key=get_implant_slot):
    729             ImplantEntry(parent=self.implant_list, align=uiconst.TOTOP, type_id=type_id, padBottom=2)
    730 
    731         self.implant_list.EnableAutoSize()
    732 
    733     def on_expand(self, clone_id):
    734         if clone_id == self.clone.id:
    735             self.expand_implants()
    736 
    737     def on_collapse(self, clone_id):
    738         if clone_id == self.clone.id:
    739             self.collapse_implants()
    740 
    741     def expand_implants(self):
    742         if not self.clone.has_implants:
    743             return
    744         self.create_implant_list()
    745         self.implant_list.DisableAutoSize()
    746         self.implant_list.GetAbsoluteSize()
    747 
    748         def after_animation():
    749             self.implant_list.EnableAutoSize()
    750             self.on_expand_anim_finished(self)
    751 
    752         _, height = self.implant_list.GetAutoSize()
    753         animations.FadeTo(self.implant_list, startVal=0.0, endVal=1.0, duration=0.15)
    754         animations.MorphScalar(self.implant_list, 'height', endVal=height, duration=0.15, callback=after_animation)
    755 
    756     def collapse_implants(self):
    757         if self.implant_list:
    758             self.implant_list.DisableAutoSize()
    759             animations.FadeOut(self.implant_list, duration=0.15)
    760             animations.MorphScalar(self.implant_list, 'height', startVal=self.implant_list.height, endVal=0, duration=0.15, callback=self.implant_list.Close)
    761             self.implant_list = None
    762 
    763     def set_clone_value_hint(self, icon):
    764         if not self.clone.implants:
    765             icon.hint = jump_clone_label('EstimatedCloneLabel', cloneValue=FmtISK(0, 0))
    766             return
    767         total = 0
    768         for type_id in self.clone.implants:
    769             price = GetAveragePrice(type_id)
    770             if price:
    771                 total += price
    772 
    773         if total:
    774             cloneValue = FmtISK(total, 0)
    775         else:
    776             cloneValue = localization.GetByLabel('UI/Common/Unknown')
    777         icon.hint = jump_clone_label('EstimatedCloneLabel', cloneValue=cloneValue)
    778 
    779 
    780 class ImplantsOverview(layoutGrid.LayoutGrid):
    781     default_state = uiconst.UI_NORMAL
    782     EXPAND_ICON_OPACITY_IDLE = 0.25
    783     EXPAND_ICON_OPACITY_HOVER = 1.0
    784 
    785     def __init__(self, controller, clone, **kwargs):
    786         kwargs['columns'] = 2
    787         kwargs['cellSpacing'] = 4
    788         super(ImplantsOverview, self).__init__(**kwargs)
    789         self.controller = controller
    790         self.clone = clone
    791         self.expand_icon = None
    792         self.implant_cont = None
    793         self.layout()
    794         self.controller.on_expand.connect(self.on_expand)
    795         self.controller.on_collapse.connect(self.on_collapse)
    796 
    797     @property
    798     def is_expanded(self):
    799         return self.controller.is_expanded(self.clone.id)
    800 
    801     def layout(self):
    802         self.implant_cont = ContainerAutoSize(parent=self, align=uiconst.TOPLEFT, opacity=self.get_implant_opacity())
    803         self.expand_icon = themeColored.SpriteThemeColored(parent=container.Container(parent=self, align=uiconst.CENTER, pos=(0, 0, 16, 16)), align=uiconst.CENTER, state=uiconst.UI_DISABLED, width=9, height=9, texturePath='res:/UI/Texture/Shared/arrows/arrowLeft.png', rotation=self.get_icon_rotation(), colorType=uiconst.COLORTYPE_UIHILIGHTGLOW, opacity=self.EXPAND_ICON_OPACITY_IDLE)
    804         icon_size = 24
    805         icon_gap = 2
    806         left = 0
    807         for type_id in sorted(self.clone.implants, key=get_implant_slot):
    808             icon = itemIcon.ItemIcon(parent=self.implant_cont, align=uiconst.TOPLEFT, pos=(left,
    809              0,
    810              icon_size,
    811              icon_size), typeID=type_id, showOmegaOverlay=False, showPrice=True)
    812             icon.techIcon.Disable()
    813             icon.techIcon.width = 12
    814             icon.techIcon.height = 12
    815             icon.OnClick = self.OnClick
    816             if self.is_expanded:
    817                 icon.Disable()
    818             left += icon_size + icon_gap
    819 
    820     def on_expand(self, clone_id):
    821         if clone_id != self.clone.id:
    822             return
    823         self._update_implant_icons_state()
    824         self._anim_update_state()
    825         tooltipUtil.RefreshTooltipForOwner(self)
    826 
    827     def on_collapse(self, clone_id):
    828         if clone_id != self.clone.id:
    829             return
    830         self._update_implant_icons_state()
    831         self._anim_update_state()
    832 
    833     def _update_implant_icons_state(self):
    834         for implant_icon in self.implant_cont.children:
    835             if self.is_expanded:
    836                 implant_icon.Disable()
    837             else:
    838                 implant_icon.Enable()
    839 
    840     def _anim_update_state(self):
    841         animations.MorphScalar(self.expand_icon, 'rotation', startVal=self.expand_icon.rotation, endVal=self.get_icon_rotation(), duration=0.1)
    842         animations.FadeTo(self.implant_cont, startVal=self.implant_cont.opacity, endVal=self.get_implant_opacity(), duration=0.15)
    843 
    844     def get_icon_rotation(self):
    845         if self.is_expanded:
    846             return -math.pi / 2.0
    847         return math.pi / 2.0
    848 
    849     def get_implant_opacity(self):
    850         if self.is_expanded:
    851             return 0.1
    852         return 1.0
    853 
    854     def OnClick(self, *args):
    855         self.controller.toggle_expanded(self.clone.id)
    856 
    857     def OnMouseEnter(self):
    858         animations.FadeIn(self.expand_icon, endVal=self.EXPAND_ICON_OPACITY_HOVER, duration=0.15)
    859 
    860     def OnMouseExit(self):
    861         animations.FadeTo(self.expand_icon, startVal=self.expand_icon.opacity, endVal=self.EXPAND_ICON_OPACITY_IDLE, duration=0.25)
    862 
    863 
    864 class ImplantEntry(containerAutoSize.ContainerAutoSize):
    865     default_alignMode = uiconst.TOPLEFT
    866 
    867     def __init__(self, type_id, **kwargs):
    868         super(ImplantEntry, self).__init__(**kwargs)
    869         self.type_id = type_id
    870         self.info_icon = None
    871         self.label = None
    872         self.label_cont = None
    873         self.layout()
    874 
    875     def layout(self):
    876         icon = itemIcon.ItemIcon(parent=self, align=uiconst.CENTERLEFT, state=uiconst.UI_DISABLED, height=16, width=16, typeID=self.type_id, showOmegaOverlay=False)
    877         icon.techIcon.width = 6
    878         icon.techIcon.height = 6
    879         self.label_cont = containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TOPLEFT, state=uiconst.UI_NORMAL, left=20, clipChildren=True)
    880         self.label_cont.GetMenu = self.GetMenu
    881         self.label_cont.GetHint = self.GetHint
    882         self.label_cont.tooltipPointer = uiconst.POINT_LEFT_1
    883         self.label = eveLabel.EveLabelMedium(parent=self.label_cont, align=uiconst.TOPLEFT, state=uiconst.UI_DISABLED, text=evetypes.GetName(self.type_id), autoFadeSides=16)
    884         self.info_icon = infoIcon.InfoIcon(parent=self, align=uiconst.CENTERLEFT, typeID=self.type_id)
    885 
    886     def GetMenu(self):
    887         return sm.GetService('menu').GetMenuFromItemIDTypeID(itemID=None, typeID=self.type_id, includeMarketDetails=True)
    888 
    889     def GetHint(self):
    890         ret = u'{}<br>{}'.format(eveformat.bold(evetypes.GetName(self.type_id)), evetypes.GetDescription(self.type_id))
    891         price = GetAveragePrice(self.type_id)
    892         if price:
    893             ret += '\n\n%s' % localization.GetByLabel('UI/SkillTrading/EstimatedPrice', price=FmtISK(price, False))
    894         return ret
    895 
    896     def UpdateAlignment(self, budgetLeft = 0, budgetTop = 0, budgetWidth = 0, budgetHeight = 0, updateChildrenOnly = False):
    897         self.label_cont.maxWidth = budgetWidth - self.label_cont.left - self.info_icon.width - 4
    898         budget = super(ImplantEntry, self).UpdateAlignment(budgetLeft=budgetLeft, budgetTop=budgetTop, budgetWidth=budgetWidth, budgetHeight=budgetHeight, updateChildrenOnly=updateChildrenOnly)
    899         self.info_icon.left = self.label_cont.left + self.label_cont.width + 4
    900         return budget
    901 
    902 
    903 class JumpCloneIcon(container.Container):
    904     default_height = 64
    905     default_width = 64
    906 
    907     def __init__(self, clone, **kwargs):
    908         super(JumpCloneIcon, self).__init__(**kwargs)
    909         self.clone = clone
    910         self.layout()
    911 
    912     def layout(self):
    913         sprite.Sprite(parent=self, align=uiconst.TOALL, state=uiconst.UI_DISABLED, texturePath='res:/ui/Texture/Icons/127_64_1.png', spriteEffect=trinity.TR2_SFX_COLOROVERLAY, color=self.generate_clone_color())
    914         if self.clone.in_ship:
    915             sprite.Sprite(parent=self, align=uiconst.BOTTOMRIGHT, width=32, height=32, texturePath='res:/UI/Texture/Shared/circularGradient.png', rotation=-math.pi / 2.0, color=(0.0, 0.0, 0.0), idx=0, opacity=0.5)
    916             sprite.Sprite(parent=self, align=uiconst.BOTTOMRIGHT, width=32, height=32, texturePath='res:/UI/Texture/Icons/9_64_5.png', hint=jump_clone_label('CloneInShipHint'), idx=0)
    917 
    918     def generate_clone_color(self):
    919         r = random.Random(self.clone.id)
    920         return (r.random() * 0.4 + 0.4, r.random() * 0.1 + 0.4, r.random() * 0.2 + 0.4)
    921 
    922 
    923 def load_errors_tooltip(panel, parent, errors):
    924     if not errors:
    925         return
    926     panel.LoadGeneric1ColumnTemplate()
    927     panel.margin = (8, 8, 8, 8)
    928     panel.cellSpacing = (0, 4)
    929     for error in errors:
    930         label = panel.AddLabelMedium(text=error, wrapWidth=300, padding=(8, 4, 8, 4))
    931         sprite.Sprite(bgParent=label.parent, texturePath='res:/UI/Texture/Classes/Industry/Output/hatchPattern.png', tileX=True, tileY=True, color=eveColor.DANGER_RED, opacity=0.1)
    932 
    933 
    934 def load_clone_implants_tooltip(panel, parent, clone):
    935     panel.LoadGeneric1ColumnTemplate()
    936     panel.margin = (8, 8, 8, 8)
    937     if not clone.has_implants:
    938         panel.AddLabelMedium(text=jump_clone_label('NoImplantsInstalled'))
    939         return
    940     list_cont = containerAutoSize.ContainerAutoSize(width=260)
    941     panel.state = uiconst.UI_NORMAL
    942     for i, type_id in enumerate(sorted(clone.implants, key=get_implant_slot)):
    943         ItemEntry(parent=list_cont, align=uiconst.TOTOP, typeID=type_id, padTop=8 if i > 0 else 0)
    944 
    945     panel.AddCell(list_cont)
    946 
    947 
    948 def get_implant_slot(type_id):
    949     return getattr(sm.GetService('godma').GetType(type_id), 'implantness', None)
    950 
    951 
    952 class ItemEntry(containerAutoSize.ContainerAutoSize):
    953     default_alignMode = uiconst.TOTOP
    954     default_state = uiconst.UI_NORMAL
    955     default_minHeight = 32
    956 
    957     def ApplyAttributes(self, attributes):
    958         super(ItemEntry, self).ApplyAttributes(attributes)
    959         self.typeID = attributes.typeID
    960         itemIcon.ItemIcon(parent=container.Container(parent=self, align=uiconst.TOLEFT, width=32), align=uiconst.CENTERTOP, state=uiconst.UI_DISABLED, pos=(0, 0, 32, 32), typeID=self.typeID)
    961         infoIcon.InfoIcon(parent=container.Container(parent=self, align=uiconst.TORIGHT, width=16), align=uiconst.CENTERTOP, top=8, typeID=self.typeID)
    962         eveLabel.EveLabelMedium(parent=VerticalCenteredContainer(parent=self, align=uiconst.TOTOP, minHeight=32), align=uiconst.TOTOP, padding=(8, 0, 8, 0), text=evetypes.GetName(self.typeID))
    963 
    964     def GetMenu(self):
    965         return GetMenuService().GetMenuFromItemIDTypeID(None, self.typeID, includeMarketDetails=True)
    966 
    967 
    968 class VerticalCenteredContainer(containerAutoSize.ContainerAutoSize):
    969 
    970     def __init__(self, **kwargs):
    971         kwargs['callback'] = self.on_size_change
    972         super(VerticalCenteredContainer, self).__init__(**kwargs)
    973 
    974     def on_size_change(self):
    975         if self.children:
    976             content_height = 0
    977             for i, child in enumerate(self.children):
    978                 content_height += child.height + child.padTop + child.padBottom
    979                 if i > 0:
    980                     content_height += child.top
    981 
    982             adjusted_top = int(round((self.height - content_height) / 2.0))
    983             self.children[0].top = adjusted_top
    984 
    985 
    986 class JumpCooldownBanner(containerAutoSize.ContainerAutoSize):
    987     default_name = 'JumpCooldownBanner'
    988     default_alignMode = uiconst.TOTOP
    989 
    990     def __init__(self, controller, **kwargs):
    991         super(JumpCooldownBanner, self).__init__(**kwargs)
    992         self.controller = controller
    993         self.layout()
    994         self.update_state()
    995         self.controller.on_update.connect(self.update_state)
    996 
    997     def layout(self):
    998         self.frame = themeColored.FrameThemeColored(bgParent=self, texturePath='res:/UI/Texture/Shared/DarkStyle/panel2Corner_Solid.png', cornerSize=9, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.15)
    999         self.corner = themeColored.SpriteThemeColored(parent=self, align=uiconst.TOPLEFT, state=uiconst.UI_DISABLED, texturePath='res:/UI/Texture/Shared/DarkStyle/cornerTriSmall.png', width=5, height=5, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.5)
   1000         self.icon = themeColored.SpriteThemeColored(parent=self, align=uiconst.CENTERLEFT, state=uiconst.UI_HIDDEN, width=32, height=32, left=10, texturePath='res:/UI/Texture/classes/agency/iconExclamation.png', colorType=uiconst.COLORTYPE_UIHILIGHT)
   1001         self.label = eveLabel.EveLabelMedium(parent=self, align=uiconst.TOTOP, state=uiconst.UI_DISABLED, padding=(16, 8, 8, 8))
   1002         more_info = infoIcon.MoreInfoIcon(parent=self, align=uiconst.CENTERRIGHT, left=16)
   1003         more_info.LoadTooltipPanel = self.LoadInfoTooltip
   1004 
   1005     def update_state(self):
   1006         self._update_state_inner()
   1007         self._auto_update_state()
   1008 
   1009     def LoadInfoTooltip(self, panel, parent):
   1010         panel.state = uiconst.UI_NORMAL
   1011         panel.LoadGeneric2ColumnTemplate()
   1012         panel.margin = (8, 8, 8, 8)
   1013         cooldown_hours = int(self.controller.jump_cooldown_duration.total_seconds() / 3600)
   1014         panel.AddLabelMedium(text=jump_clone_label('JumpCooldownHint', cooldown=cooldown_hours, color=eveThemeColor.THEME_ACCENT.hex_argb), colSpan=2, width=300)
   1015         panel.AddSpacer(height=8, colSpan=2)
   1016         for type_id in CLONE_JUMP_COOLDOWN_SKILLS:
   1017             panel.AddRow(rowClass=infoBubble.SkillEntry, typeID=type_id, level=0, showLevel=False, skillBarPadding=(24, 8, 0, 8))
   1018 
   1019     def _update_state_inner(self):
   1020         if not self.controller.is_jump_available:
   1021             self.label.SetText(u'{}\n{}'.format(jump_clone_label('NextCloneJump'), localization.formatters.FormatTimeIntervalShortWritten(datetimeutils.timedelta_to_filetime_delta(self.controller.remaining_time_until_next_jump), showFrom='day', showTo='second')))
   1022             self.label.padLeft = 52
   1023             self.icon.display = True
   1024             self.set_color(eveColor.WARNING_ORANGE)
   1025         else:
   1026             self.label.SetText(jump_clone_label('CloneJumpAvailable'))
   1027             self.label.padLeft = 16
   1028             self.icon.display = False
   1029             self.set_color(None)
   1030 
   1031     def set_color(self, color):
   1032         self.frame.SetFixedColor(color)
   1033         self.corner.SetFixedColor(color)
   1034         self.icon.SetFixedColor(color)
   1035         if color is None:
   1036             self.label.SetRGBA(*self.label.default_color)
   1037         else:
   1038             self.label.SetRGBA(*color)
   1039 
   1040     @threadutils.highlander_threaded
   1041     def _auto_update_state(self):
   1042         while not self.destroyed:
   1043             self._update_state_inner()
   1044             if self.controller.is_jump_available:
   1045                 return
   1046             uthread2.sleep(1.0)
   1047 
   1048 
   1049 class InstallCloneBanner(containerAutoSize.ContainerAutoSize):
   1050     default_alignMode = uiconst.TOTOP
   1051 
   1052     def __init__(self, controller, **kwargs):
   1053         super(InstallCloneBanner, self).__init__(**kwargs)
   1054         self.controller = controller
   1055         self.label = None
   1056         self.install_button = None
   1057         self.layout()
   1058         self.update()
   1059         self.controller.on_update.connect(self.update)
   1060 
   1061     def layout(self):
   1062         themeColored.FrameThemeColored(parent=self, texturePath='res:/UI/Texture/Shared/DarkStyle/panel2Corner_Solid.png', cornerSize=9, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.15)
   1063         themeColored.SpriteThemeColored(parent=self, align=uiconst.TOPLEFT, state=uiconst.UI_DISABLED, texturePath='res:/UI/Texture/Shared/DarkStyle/cornerTriSmall.png', width=5, height=5, colorType=uiconst.COLORTYPE_UIHILIGHT, opacity=0.5)
   1064         button_cont = containerAutoSize.ContainerAutoSize(parent=self, align=uiconst.TORIGHT, padding=(16, 8, 16, 8))
   1065         self.install_button = Button(parent=button_cont, align=uiconst.CENTER, label=localization.GetByLabel('UI/Medical/InstallJumpClone'), func=lambda button: self.controller.install())
   1066         self.install_button.Disable()
   1067         self.label = eveLabel.EveLabelMedium(parent=self, align=uiconst.TOTOP, state=uiconst.UI_NORMAL, padding=(16, 8, 0, 8))
   1068 
   1069     def get_text(self):
   1070         if not self.controller.is_docked:
   1071             return eveformat.color(jump_clone_label('NotDocked'), color=COLOR_TEXT_SECONDARY)
   1072         else:
   1073             docked_in = jump_clone_label('DockedIn', location=evelink.location_link(self.controller.location_id))
   1074             if self.controller.is_clone_installed_here:
   1075                 subtext = jump_clone_label('InstalledCloneCount', clone_count=self.controller.clone_count_installed_here)
   1076             else:
   1077                 subtext = localization.GetByLabel('UI/Medical/NoJumpCloneInstalled')
   1078             return u'{}<br>{}'.format(docked_in, subtext)
   1079 
   1080     @threadutils.highlander_threaded
   1081     def update(self):
   1082         self.label.SetText(self.get_text())
   1083         errors = self.controller.validate_install_clone()
   1084         if errors:
   1085             self.install_button.Disable()
   1086             self.install_button.LoadTooltipPanel = functools.partial(load_errors_tooltip, errors=errors)
   1087         else:
   1088             self.install_button.Enable()
   1089             self.install_button.LoadTooltipPanel = None