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