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

cosmeticSlot.py (24578B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\cosmetics\ship\pages\studio\cosmeticSlot.py
      2 import eveicon
      3 import evetypes
      4 import gametime
      5 import math
      6 from carbon.client.script.environment.AudioUtil import PlaySound
      7 from carbon.common.script.sys.serviceConst import ROLE_GML
      8 from carbonui import Align, uiconst, PickState, SpriteEffect
      9 from carbonui.control.contextMenu.menuData import MenuData
     10 from carbonui.loggers.buttonLogger import log_button_clicked
     11 from carbonui.primitives.container import Container
     12 from carbonui.primitives.containerAutoSize import ContainerAutoSize
     13 from carbonui.primitives.sprite import Sprite
     14 from carbonui.primitives.transform import Transform
     15 from carbonui.uianimations import animations
     16 from carbonui.uiconst import OutputMode
     17 from cosmetics.client.shipSkinComponentSvc import get_ship_skin_component_svc
     18 from cosmetics.client.shipSkinSequencingSvc import get_ship_skin_sequencing_svc
     19 from cosmetics.client.ships import ship_skin_signals
     20 from cosmetics.client.ships.skins.live_data import current_skin_design_signals, current_skin_design
     21 from cosmetics.common.ships.skins.live_data.component_license_type import ComponentLicenseType
     22 from cosmetics.common.ships.skins.static_data.slot_name import PATTERN_SLOT_IDS, PATTERN_SLOT_ID_BY_PATTERN_MATERIAL_ID
     23 from eve.client.script.ui import eveThemeColor, eveColor
     24 from eve.client.script.ui.cosmetics.ship import const as shipConst
     25 from eve.client.script.ui.cosmetics.ship.pages.sequence import sequence_ui_signals
     26 from eve.client.script.ui.cosmetics.ship.pages.studio import nebulaConsts
     27 from eve.client.script.ui.cosmetics.ship.pages.studio.licenseSelector import LicenseSelector
     28 from eve.client.script.ui.cosmetics.ship.pages.studio.radiallyAlignedMixin import RadiallyAlignedMixin
     29 from eve.client.script.ui.cosmetics.ship.pages.studio.requiredComponentTooltipPanel import RequiredComponentTooltipPanel
     30 from eveservices.menu import GetMenuService
     31 from localization import GetByLabel
     32 from signals import Signal
     33 OPACITY_FRAME_IDLE = 0.1
     34 OPACITY_FRAME_HOVER = 0.2
     35 OPACITY_FRAME_SELECTED = 0.3
     36 OPACITY_FRAME_IDLE_NEBULA = 0.4
     37 OPACITY_FRAME_HOVER_NEBULA = 0.5
     38 OPACITY_FRAME_SELECTED_NEBULA = 0.6
     39 
     40 def get_frame_color(showing_warning):
     41     if showing_warning:
     42         return eveColor.DANGER_RED
     43     else:
     44         return eveThemeColor.THEME_FOCUSDARK
     45 
     46 
     47 def get_side_frame_color(showing_warning):
     48     return eveThemeColor.THEME_FOCUSDARK
     49 
     50 
     51 def get_anim_time_offset(duration):
     52     seconds = gametime.GetWallclockTimeNow() / float(gametime.SEC)
     53     return duration - seconds % duration
     54 
     55 
     56 class CosmeticSlotButton(Container, RadiallyAlignedMixin):
     57     default_width = 64
     58     default_height = 64
     59     default_state = uiconst.UI_NORMAL
     60     default_align = Align.CENTER
     61     analyticID = None
     62 
     63     def __init__(self, texture_path = None, icon_size = 32, is_selectable = True, **kw):
     64         super(CosmeticSlotButton, self).__init__(**kw)
     65         self.is_selectable = is_selectable
     66         self.is_selected = False
     67         self.is_hovered = False
     68         self.showing_warning = False
     69         self.construct_icon(texture_path, icon_size)
     70         self.construct_hover_frame()
     71         self.construct_frame()
     72         self.on_click = Signal('on_click')
     73         self.on_mouse_enter = Signal('on_mouse_enter')
     74         self.on_mouse_exit = Signal('on_mouse_exit')
     75         self.on_selected = Signal('on_selected')
     76         self.on_deselected = Signal('on_deselected')
     77         nebulaConsts.SELECTED_SKINR_BACKGROUND.on_change.connect(self.on_nebula_changed)
     78 
     79     def Close(self):
     80         try:
     81             nebulaConsts.SELECTED_SKINR_BACKGROUND.on_change.disconnect(self.on_nebula_changed)
     82         finally:
     83             super(CosmeticSlotButton, self).Close()
     84 
     85     def OnClick(self, *args):
     86         self.on_click()
     87         log_button_clicked(self)
     88 
     89     def OnMouseEnter(self, *args):
     90         self.is_hovered = True
     91         self.update_state()
     92         self.on_mouse_enter()
     93 
     94     def OnMouseExit(self, *args):
     95         self.is_hovered = False
     96         self.update_state()
     97         self.on_mouse_exit()
     98 
     99     def construct_icon(self, texture_path, icon_size):
    100         self.icon = Sprite(name='icon', parent=self, align=Align.CENTER, state=uiconst.UI_DISABLED, texturePath=texture_path, pos=(0,
    101          0,
    102          icon_size,
    103          icon_size))
    104         Sprite(bgParent=self, texturePath='res:/UI/Texture/circle_full.png', padding=8, color=eveThemeColor.THEME_FOCUSDARK, opacity=0.2)
    105         Sprite(bgParent=self, texturePath='res:/UI/Texture/circle_full.png', padding=8, color=eveColor.BLACK, opacity=0.4)
    106 
    107     def construct_hover_frame(self):
    108         self.hover_frame = Transform(parent=self, align=Align.TOALL, pickState=PickState.OFF, padding=4, opacity=0.0, scalingCenter=(0.5, 0.5))
    109         self.hover_frame_sprite = Sprite(name='hover_frame_sprite', bgParent=self.hover_frame, texturePath='res:/UI/Texture/Classes/Cosmetics/Ship/Pages/Studio/slot_hover_frame.png', outputMode=OutputMode.COLOR_AND_GLOW, glowBrightness=0.2, color=eveThemeColor.THEME_ACCENT)
    110 
    111     def construct_frame(self):
    112         self.frame = Transform(name='frame', parent=self, align=Align.TOALL, pickState=PickState.OFF, padding=-6, opacity=OPACITY_FRAME_IDLE if nebulaConsts.SELECTED_SKINR_BACKGROUND.get() is nebulaConsts.BackgroundEnum.Empty else OPACITY_FRAME_IDLE_NEBULA, scalingCenter=(0.5, 0.5))
    113         self.frame_bg = Sprite(bgParent=self.frame, texturePath='res:/UI/Texture/Classes/Cosmetics/Ship/Pages/Studio/slot_frame.png', outputMode=OutputMode.COLOR_AND_GLOW, glowBrightness=0.2, color=get_frame_color(self.showing_warning))
    114 
    115     def set_selected(self):
    116         self.is_selected = True
    117         self.on_selected()
    118 
    119     def set_deselected(self):
    120         self.is_selected = False
    121         self.on_deselected()
    122 
    123     def update_state(self, animate = True):
    124         self._update_state_frame(animate)
    125         self._update_state_hover_frame(animate)
    126 
    127     def _update_state_hover_frame(self, animate):
    128         opacity = 1.0 if self.is_selected else 0.0
    129         if self.is_selected:
    130             scale = (1.0, 1.0)
    131         else:
    132             scale = (0.7, 0.7)
    133         if animate:
    134             animations.FadeTo(self.hover_frame, self.hover_frame.opacity, opacity, duration=2 * uiconst.TIME_ENTRY)
    135             animations.MorphVector2(self.hover_frame, 'scale', self.hover_frame.scale, scale, duration=2 * uiconst.TIME_ENTRY)
    136         else:
    137             self.hover_frame.opacity = opacity
    138             self.hover_frame.scale = scale
    139 
    140     def _update_state_frame(self, animate = True):
    141         has_nebula_background = nebulaConsts.SELECTED_SKINR_BACKGROUND.get() is not nebulaConsts.BackgroundEnum.Empty
    142         if self.is_selected:
    143             opacity = OPACITY_FRAME_SELECTED if not has_nebula_background else OPACITY_FRAME_SELECTED_NEBULA
    144         elif self.is_hovered:
    145             opacity = OPACITY_FRAME_HOVER if not has_nebula_background else OPACITY_FRAME_HOVER_NEBULA
    146         else:
    147             opacity = OPACITY_FRAME_IDLE if not has_nebula_background else OPACITY_FRAME_IDLE_NEBULA
    148         rotation = 0.0 if self.is_hovered or self.is_selected else -math.pi / 2
    149         scale = (1.06, 1.06) if self.is_selected else (1.0, 1.0)
    150         self.frame_bg.rgb = get_frame_color(self.showing_warning)[:3]
    151         if animate:
    152             animations.FadeTo(self.frame, self.frame.opacity, opacity, duration=uiconst.TIME_ENTRY)
    153             animations.MorphScalar(self.frame, 'rotation', self.frame.rotation, rotation, duration=0.15)
    154             animations.MorphVector2(self.frame, 'scale', self.frame.scale, scale, duration=0.15)
    155         else:
    156             self.frame.opacity = opacity
    157             self.frame.rotation = rotation
    158             self.frame.scale = scale
    159 
    160     def on_nebula_changed(self, value):
    161         has_nebula = value is not nebulaConsts.BackgroundEnum.Empty
    162         if self.is_selected:
    163             opacity = OPACITY_FRAME_SELECTED if has_nebula is False else OPACITY_FRAME_SELECTED_NEBULA
    164         elif self.is_hovered:
    165             opacity = OPACITY_FRAME_HOVER if has_nebula is False else OPACITY_FRAME_HOVER_NEBULA
    166         else:
    167             opacity = OPACITY_FRAME_IDLE if has_nebula is False else OPACITY_FRAME_IDLE_NEBULA
    168         animations.FadeTo(self.frame, self.frame.opacity, opacity, duration=uiconst.TIME_ENTRY)
    169 
    170 
    171 class CosmeticSlot(Container, RadiallyAlignedMixin):
    172     default_width = 256
    173     default_height = 256
    174     default_align = Align.CENTER
    175     default_pickState = PickState.CHILDREN
    176     default_display = True
    177     _radial_offset = 20
    178     has_side_frame = True
    179     showing_warning = False
    180 
    181     def __init__(self, slot_data, component = None, selectable = True, license_selector_align = Align.CENTERBOTTOM, *args, **kwargs):
    182         super(CosmeticSlot, self).__init__(*args, **kwargs)
    183         self.slot_data = slot_data
    184         self.is_selectable = selectable
    185         self._selected_component = component
    186         self._license_selector_align = license_selector_align
    187         self.construct_layout()
    188         self.connect_signals()
    189         self.update_icon()
    190         self.update_warning()
    191         self._update_state(animate=False)
    192 
    193     def Close(self):
    194         try:
    195             self.disconnect_signals()
    196         finally:
    197             super(CosmeticSlot, self).Close()
    198 
    199     def connect_signals(self):
    200         current_skin_design_signals.on_slot_selected.connect(self.on_slot_selected)
    201         current_skin_design_signals.on_slot_fitting_changed.connect(self.on_slot_fitting_changed)
    202         sequence_ui_signals.on_num_skins_changed.connect(self.on_num_skins_changed)
    203         sequence_ui_signals.on_component_license_selection_changed.connect(self.on_component_license_selection_changed)
    204         ship_skin_signals.on_component_license_granted.connect(self.on_component_license_granted)
    205         ship_skin_signals.on_component_license_cache_invalidated.connect(self.on_component_license_cache_invalidated)
    206         nebulaConsts.SELECTED_SKINR_BACKGROUND.on_change.connect(self.on_nebula_changed)
    207 
    208     def disconnect_signals(self):
    209         current_skin_design_signals.on_slot_selected.disconnect(self.on_slot_selected)
    210         current_skin_design_signals.on_slot_fitting_changed.disconnect(self.on_slot_fitting_changed)
    211         sequence_ui_signals.on_num_skins_changed.disconnect(self.on_num_skins_changed)
    212         sequence_ui_signals.on_component_license_selection_changed.disconnect(self.on_component_license_selection_changed)
    213         ship_skin_signals.on_component_license_granted.disconnect(self.on_component_license_granted)
    214         ship_skin_signals.on_component_license_cache_invalidated.disconnect(self.on_component_license_cache_invalidated)
    215         nebulaConsts.SELECTED_SKINR_BACKGROUND.on_change.disconnect(self.on_nebula_changed)
    216 
    217     def on_num_skins_changed(self, num_skins):
    218         self._check_show_not_owned_warning()
    219         self.update_warning()
    220 
    221     def on_component_license_selection_changed(self):
    222         self._check_show_not_owned_warning()
    223         self.update_warning()
    224 
    225     def on_component_license_granted(self, *args):
    226         self._check_show_not_owned_warning()
    227         self.update_warning()
    228 
    229     def on_component_license_cache_invalidated(self, _licenses_flushed):
    230         self._check_show_not_owned_warning()
    231         self.update_warning()
    232 
    233     def construct_layout(self):
    234         self.construct_button()
    235         if self.has_side_frame:
    236             self.construct_side_frame()
    237         self.construct_license_selector()
    238         self.construct_pulse_highlight()
    239 
    240     def construct_button(self):
    241         self.button = CosmeticSlotButton(parent=self, align=Align.CENTER, is_selectable=self.is_selectable)
    242         if self.slot_data.unique_id in PATTERN_SLOT_IDS:
    243             mask_texture = 'res:/UI/Texture/circle_full.png'
    244         else:
    245             mask_texture = 'res:/UI/Texture/classes/Cosmetics/Ship/pages/studio/nanocoating_mask.png'
    246         self.button.icon.spriteEffect = SpriteEffect.MODULATE
    247         self.button.icon.SetSecondaryTexturePath(mask_texture)
    248         self.button.on_click.connect(self.OnSlotClick)
    249         self.button.on_mouse_enter.connect(self.OnSlotMouseEnter)
    250         self.button.on_mouse_exit.connect(self.OnSlotMouseExit)
    251         self.button.on_selected.connect(self.on_button_selected)
    252         self.button.on_deselected.connect(self.on_button_deselected)
    253         self.button.GetMenu = self.GetSlotMenu
    254         self.button.ConstructTooltipPanel = self.ConstructButtonTooltipPanel
    255 
    256     def ConstructButtonTooltipPanel(self, *args):
    257         return RequiredComponentTooltipPanel(component_instance=self.selected_component, title=self.slot_data.name, text=self.get_fitted_component_name(), show_warning=self.showing_warning)
    258 
    259     def get_fitted_component_name(self):
    260         if self.selected_component:
    261             return self.selected_component.get_component_data().name
    262         else:
    263             return GetByLabel('UI/OrbitalSkyhook/ConfigWnd/NothingSelected')
    264 
    265     def on_button_selected(self):
    266         self._update_state()
    267 
    268     def on_button_deselected(self):
    269         self._update_state()
    270 
    271     def construct_side_frame(self):
    272         self.side_frame = Container(name='side_frame', parent=self, align=Align.CENTER, pickState=PickState.OFF, pos=(-54, 0, 39, 36))
    273         Sprite(parent=self.side_frame, pos=(-2, 0, 24, 24), align=Align.CENTER, texturePath=shipConst.SLOT_ICONS[self.slot_data.unique_id])
    274         self.side_frame_bg = Sprite(bgParent=self.side_frame, texturePath='res:/UI/Texture/Classes/Cosmetics/Ship/Pages/Studio/slot_side_frame.png', color=get_side_frame_color(self.showing_warning), glowBrightness=0.2, outputMode=uiconst.OUTPUT_COLOR_AND_GLOW)
    275 
    276     def construct_license_selector(self):
    277         if self._license_selector_align == Align.CENTERTOP:
    278             align = Align.CENTER
    279             left = 0
    280             top = -53
    281         elif self._license_selector_align == Align.CENTERBOTTOM:
    282             align = Align.CENTER
    283             left = 0
    284             top = 53
    285         elif self._license_selector_align == Align.CENTERLEFT:
    286             align = Align.CENTERRIGHT
    287             left = 174
    288             top = 0
    289         elif self._license_selector_align == Align.CENTERRIGHT:
    290             align = Align.CENTERLEFT
    291             left = 174
    292             top = 0
    293         self.license_selector_container = ContainerAutoSize(name='license_selector_container', parent=self, align=align, height=20, left=left, top=top, display=False)
    294         self.license_selector = LicenseSelector(name='license_selector', parent=self.license_selector_container, component_instance=self.selected_component)
    295 
    296     def construct_pulse_highlight(self):
    297         self.pulse_highlight = Transform(name='pulse_highlight', parent=self, align=Align.CENTER, pickState=PickState.OFF, scalingCenter=(0.5, 0.5), pos=(0, 0, 120, 120), display=False)
    298         self.pulse_highlight_bg = Sprite(bgParent=self.pulse_highlight, texturePath='res:/UI/Texture/Classes/Cosmetics/Ship/Pages/Studio/slot_pulse_highlight.png')
    299 
    300     def show_pulse_highlight(self):
    301         self.pulse_highlight.display = True
    302         scale_contracted = (0.85, 0.85)
    303         scale_expanded = (1.1, 1.1)
    304         duration = 5.0
    305         animation_curve = ((0.0, scale_contracted),
    306          (0.5 / duration, scale_contracted),
    307          (2.5 / duration, scale_expanded),
    308          (3.0 / duration, scale_expanded),
    309          (1.0, scale_contracted))
    310         animations.MorphVector2(obj=self.pulse_highlight, attrName='scale', duration=5.0, curveType=animation_curve, loops=uiconst.ANIM_REPEAT, timeOffset=get_anim_time_offset(5.0))
    311 
    312     def hide_pulse_highlight(self):
    313         animations.StopAllAnimations(self.pulse_highlight_bg)
    314         self.pulse_highlight.display = False
    315 
    316     def show_warning(self):
    317         self.showing_warning = True
    318         self.button.showing_warning = True
    319         self._update_state()
    320 
    321     def hide_warning(self):
    322         self.showing_warning = False
    323         self.button.showing_warning = False
    324         self._update_state()
    325 
    326     def update_warning(self):
    327         if not self.selected_component:
    328             return
    329         layout = current_skin_design.get().slot_layout
    330         number_of_runs = get_ship_skin_sequencing_svc().get_num_runs()
    331         num_required = number_of_runs * layout.get_amount_of_components_with_license_type(self.selected_component.component_id, ComponentLicenseType.LIMITED)
    332         license = self.selected_component.component_license_to_use or get_ship_skin_component_svc().get_default_license_to_use(self.selected_component.component_id, self.selected_component.get_component_data().category)
    333         if license is None:
    334             has_sufficient_licenses = True
    335         elif license.license_type == ComponentLicenseType.LIMITED:
    336             has_sufficient_licenses = license is None or not license.has_enough_remaining_uses(num_required)
    337         else:
    338             has_sufficient_licenses = False
    339         if has_sufficient_licenses:
    340             self.show_warning()
    341         else:
    342             self.hide_warning()
    343 
    344     def on_slot_selected(self, slot_id):
    345         if slot_id and slot_id == self.slot_data.unique_id:
    346             self.button.set_selected()
    347         else:
    348             self.button.set_deselected()
    349 
    350     def _update_state(self, animate = True):
    351         self.button.update_state(animate)
    352         if self.has_side_frame:
    353             self._update_state_side_frame(animate)
    354         self._update_state_license_selector()
    355 
    356     def _update_state_side_frame(self, animate):
    357         has_nebula_background = nebulaConsts.SELECTED_SKINR_BACKGROUND.get() is not nebulaConsts.BackgroundEnum.Empty
    358         if not self.is_fitted:
    359             opacity = 0.0
    360         elif self.is_selected:
    361             opacity = OPACITY_FRAME_SELECTED if not has_nebula_background else OPACITY_FRAME_SELECTED_NEBULA
    362         elif self.button.is_hovered:
    363             opacity = OPACITY_FRAME_HOVER if not has_nebula_background else OPACITY_FRAME_HOVER_NEBULA
    364         else:
    365             opacity = OPACITY_FRAME_IDLE if not has_nebula_background else OPACITY_FRAME_IDLE_NEBULA
    366         self.side_frame_bg.rgb = get_side_frame_color(self.showing_warning)[:3]
    367         if animate:
    368             animations.FadeTo(self.side_frame_bg, self.side_frame_bg.opacity, opacity, duration=2 * uiconst.TIME_ENTRY)
    369         else:
    370             self.side_frame_bg.opacity = opacity
    371         opacity = 1.0 if opacity > 0 else 0.0
    372         if animate:
    373             animations.FadeTo(self.side_frame, self.side_frame.opacity, opacity, duration=2 * uiconst.TIME_ENTRY)
    374         else:
    375             self.side_frame.opacity = opacity
    376 
    377     def _update_state_license_selector(self):
    378         if self.license_selector:
    379             self.license_selector.component_instance = self.selected_component
    380             self.license_selector_container.display = self.selected_component is not None and self.is_selected
    381 
    382     @property
    383     def is_selected(self):
    384         return current_skin_design.get_selected_slot_id() == self.slot_data.unique_id
    385 
    386     def on_slot_fitting_changed(self, slot_id, component_instance):
    387         self._check_show_not_owned_warning()
    388         if slot_id != self.slot_data.unique_id:
    389             return
    390         self.selected_component = component_instance
    391         self.update_icon()
    392         self._update_state()
    393 
    394     def _check_show_not_owned_warning(self):
    395         if not self.selected_component:
    396             self.hide_warning()
    397             return
    398         license = self.selected_component.component_license_to_use or get_ship_skin_component_svc().get_default_license_to_use(self.selected_component.component_id, self.selected_component.get_component_data().category)
    399         if not license:
    400             self.show_warning()
    401             return
    402         if license.license_type == ComponentLicenseType.UNLIMITED:
    403             self.hide_warning()
    404             return
    405         layout = current_skin_design.get().slot_layout
    406         number_of_runs = get_ship_skin_sequencing_svc().get_num_runs()
    407         num_required = number_of_runs * layout.get_amount_of_components_with_license_type(self.selected_component.component_id, ComponentLicenseType.LIMITED)
    408         if license.has_enough_remaining_uses(num_required):
    409             self.hide_warning()
    410         else:
    411             self.show_warning()
    412 
    413     def update_icon(self):
    414         if self.selected_component:
    415             self.button.icon.texturePath = self.selected_component.get_component_data().icon_file_path
    416             self.button.icon.width = self.button.icon.height = 48
    417         else:
    418             self.button.icon.texturePath = shipConst.SLOT_ICONS[self.slot_data.unique_id]
    419             self.button.icon.width = self.button.icon.height = 32
    420 
    421     @property
    422     def is_fitted(self):
    423         return bool(self.selected_component)
    424 
    425     def OnSlotClick(self, *args):
    426         if not self.is_selectable:
    427             return
    428         PlaySound('nanocoating_button_push_primary_play')
    429         selected_slot = None if self.is_selected else self.slot_data
    430         current_skin_design.set_selected_slot_id(selected_slot.unique_id if selected_slot else None)
    431 
    432     def OnSlotMouseEnter(self, *args):
    433         if not self.is_selectable:
    434             return
    435         PlaySound('nanocoating_button_hover_play')
    436         self._update_state()
    437 
    438     def OnSlotMouseExit(self, *args):
    439         if not self.is_selectable:
    440             return
    441         self._update_state()
    442 
    443     @property
    444     def selected_component(self):
    445         return current_skin_design.get().get_fitted_component_instance(self.slot_data.unique_id)
    446 
    447     @selected_component.setter
    448     def selected_component(self, value):
    449         self._selected_component = value
    450         self._update_state()
    451         if value:
    452             animations.MorphScalar(self.button.hover_frame_sprite, 'glowBrightness', 2.0, 0.0, duration=1.2)
    453 
    454     def GetSlotMenu(self):
    455         if not self.selected_component:
    456             return
    457         component_data = self.selected_component.get_component_data()
    458         m = MenuData()
    459         for type_id in component_data.component_item_data_by_type_id.keys():
    460             m.AddEntry(evetypes.GetName(type_id), subMenuData=GetMenuService().GetMenuFromItemIDTypeID(itemID=None, typeID=type_id, includeMarketDetails=True))
    461 
    462         if self.is_fitted and self.slot_data.unique_id not in PATTERN_SLOT_ID_BY_PATTERN_MATERIAL_ID.keys():
    463             m.AddSeparator()
    464             m.AddEntry(GetByLabel('UI/Personalization/ShipSkins/SKINR/Studio/ResetSlot'), self.unfit_slot, texturePath=eveicon.close)
    465         if self.slot_data.unique_id in PATTERN_SLOT_IDS:
    466             component_instance = current_skin_design.get().get_fitted_component_instance(self.slot_data.unique_id)
    467             if component_instance:
    468                 m.AddSeparator()
    469                 m.AddEntry(GetByLabel('UI/Personalization/ShipSkins/SKINR/Studio/ResetPatternProjection'), lambda c = component_instance: self.reset_pattern_rotation_scale_and_offset(c), texturePath=eveicon.refresh)
    470         if session.role & ROLE_GML:
    471             m.AddSeparator()
    472             component_id = self.selected_component.component_id if self.selected_component else None
    473             m.AddEntry('QA: slot_id: {}, component_id: {}'.format(self.slot_data.unique_id, component_id))
    474         return m
    475 
    476     def reset_pattern_rotation_scale_and_offset(self, component_instance):
    477         component_instance.reset_rotation_scale_and_offset()
    478         current_skin_design.add_to_undo_history()
    479 
    480     def unfit_slot(self, *args):
    481         current_skin_design.get().fit_slot(self.slot_data.unique_id, None)
    482         current_skin_design.add_to_undo_history()
    483 
    484     def show(self):
    485         self.state = uiconst.UI_PICKCHILDREN
    486         animations.FadeTo(self, self.opacity, 1.0, 0.2)
    487 
    488     def hide(self):
    489         self.state = uiconst.UI_DISABLED
    490         animations.FadeTo(self, self.opacity, 0.0, 0.25)
    491 
    492     def on_nebula_changed(self, value):
    493         if not self.has_side_frame:
    494             return
    495         has_nebula = value is not nebulaConsts.BackgroundEnum.Empty
    496         if self.is_selected:
    497             opacity = OPACITY_FRAME_SELECTED if has_nebula is False else OPACITY_FRAME_SELECTED_NEBULA
    498         elif self.button.is_hovered:
    499             opacity = OPACITY_FRAME_HOVER if has_nebula is False else OPACITY_FRAME_HOVER_NEBULA
    500         else:
    501             opacity = OPACITY_FRAME_IDLE if has_nebula is False else OPACITY_FRAME_IDLE_NEBULA
    502         animations.FadeTo(self.side_frame_bg, self.side_frame_bg.opacity, opacity, duration=2 * uiconst.TIME_ENTRY)
    503         opacity = 1.0 if opacity > 0 else 0.0
    504         animations.FadeTo(self.side_frame, self.side_frame.opacity, opacity, duration=2 * uiconst.TIME_ENTRY)
    505 
    506 
    507 class PatternCosmeticSlot(CosmeticSlot):
    508     has_side_frame = False