scrollbar.py (18913B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\carbonui\control\scrollbar.py 2 import datetime 3 import gametime 4 import mathext 5 import signals 6 import uthread2 7 from carbon.client.script.environment.AudioUtil import PlaySound 8 from carbonui import Axis, uiconst 9 from carbonui.primitives.container import Container 10 from carbonui.primitives.fill import Fill 11 from carbonui.text.color import TextColor 12 from carbonui.uianimations import animations 13 from carbonui.uicore import uicore 14 from carbonui.util.dpi import reverse_scale_dpi 15 from carbonui.util.various_unsorted import GetWindowAbove 16 from chroma import Color 17 from eve.client.script.ui import eveColor, eveThemeColor 18 19 class Scrollbar(Container): 20 TRACK_COLOR = (0.0, 0.0, 0.0) 21 TRACK_SIZE = 8 22 TRACK_OPACITY_INACTIVE = 0.0 23 TRACK_OPACITY_ACTIVE = 0.3 24 HANDLE_THICKNESS_IDLE = 2 25 HANDLE_THICKNESS_HOVER = 4 26 HANDLE_SIZE_MIN = 16 27 HANDLE_HIGHLIGHT_DURATION = datetime.timedelta(milliseconds=300) 28 PAGE_SCROLLING_INITIAL_DELAY = 0.3 29 PAGE_SCROLLING_INTERVAL = 0.05 30 _active = False 31 _handle = None 32 _handle_color_override = None 33 _handle_highlight_tasklet = None 34 _handle_highlight_timestamp = None 35 _hovered = False 36 _on_scroll_fraction_changed = None 37 _page_scrolling_tasklet = None 38 _track = None 39 40 def __init__(self, axis, scroll_fraction = 0.0, handle_size_fraction = 0.0, on_scroll_fraction_changed = None, state = uiconst.UI_NORMAL, **kwargs): 41 self._axis = axis 42 self._scroll_fraction = scroll_fraction 43 self._handle_size_fraction = handle_size_fraction 44 super(Scrollbar, self).__init__(state=state, **kwargs) 45 window_above = GetWindowAbove(self) 46 if window_above: 47 self._active = window_above.active 48 self._layout() 49 if on_scroll_fraction_changed: 50 self.on_scroll_fraction_changed.connect(on_scroll_fraction_changed) 51 52 @property 53 def axis(self): 54 return self._axis 55 56 @property 57 def scrolling(self): 58 is_page_scrolling = self._page_scrolling_tasklet is not None 59 if self._handle: 60 return self._handle.scrolling or is_page_scrolling 61 else: 62 return is_page_scrolling 63 64 @property 65 def handle_size_fraction(self): 66 return self._handle_size_fraction 67 68 @handle_size_fraction.setter 69 def handle_size_fraction(self, value): 70 value = mathext.clamp(value, 0.0, 1.0) 71 if self._handle_size_fraction != value: 72 self._handle_size_fraction = value 73 self._update_handle_size_and_position() 74 75 @property 76 def scroll_fraction(self): 77 return self._scroll_fraction 78 79 @scroll_fraction.setter 80 def scroll_fraction(self, value): 81 value = mathext.clamp(value, 0.0, 1.0) 82 if self._scroll_fraction != value: 83 self._scroll_fraction = value 84 self._update_handle_size_and_position() 85 if self._on_scroll_fraction_changed is not None: 86 self._on_scroll_fraction_changed(self) 87 88 @property 89 def on_scroll_fraction_changed(self): 90 if self._on_scroll_fraction_changed is None: 91 self._on_scroll_fraction_changed = signals.Signal('{}.on_scroll_fraction_changed'.format(self.__class__.__name__)) 92 return self._on_scroll_fraction_changed 93 94 def scroll_by_page(self, page_count = 1): 95 scrollable_portion = 1.0 - self._handle_size_fraction 96 scroll_amount = page_count * self._handle_size_fraction 97 scroll_fraction = self.scroll_fraction + scroll_amount / scrollable_portion 98 self._set_scroll_fraction_with_highlight(scroll_fraction) 99 100 def scroll_to_fraction(self, scroll_fraction): 101 self._set_scroll_fraction_with_highlight(scroll_fraction) 102 103 def _set_scroll_fraction_with_highlight(self, scroll_fraction): 104 old_scroll_fraction = self.scroll_fraction 105 self.scroll_fraction = scroll_fraction 106 if not mathext.is_close(old_scroll_fraction, scroll_fraction): 107 self._highlight_scroll_handle() 108 109 def _layout(self): 110 handle_width_min, handle_height_min = self._get_handle_size_min() 111 self._handle = ScrollHandle(parent=self, align=self._get_handle_align(), axis=self._axis, expanded=self._get_handle_expanded(), color=self._get_handle_color(), on_scroll_move=self._on_handle_scroll_move, on_hovered_changed=self._on_handle_hovered_changed, minWidth=handle_width_min, minHeight=handle_height_min) 112 self._track = Fill(bgParent=self, align=uiconst.TOALL, color=self.TRACK_COLOR, opacity=self._get_track_opacity()) 113 114 def _get_dimensions(self): 115 if self.align == uiconst.TOALL: 116 return (0, 0) 117 if self._axis == Axis.HORIZONTAL: 118 height = self.TRACK_SIZE 119 if self.align in uiconst.ALIGNMENTS_WITH_INCLUDED_VERTICAL_PADDING: 120 height += self.padTop + self.padBottom 121 return (self.width, height) 122 if self._axis == Axis.VERTICAL: 123 width = self.TRACK_SIZE 124 if self.align in uiconst.ALIGNMENTS_WITH_INCLUDED_HORIZONTAL_PADDING: 125 width += self.padLeft + self.padRight 126 return (width, self.height) 127 128 def _update_dimensions(self): 129 self.width, self.height = self._get_dimensions() 130 131 def _get_handle_align(self): 132 if self._axis == Axis.HORIZONTAL: 133 return uiconst.TOLEFT_PROP 134 if self._axis == Axis.VERTICAL: 135 return uiconst.TOTOP_PROP 136 137 def _get_handle_color(self): 138 if self._handle_color_override is not None: 139 return self._handle_color_override 140 elif self._handle and self._handle.hovered: 141 return self._get_handle_highlight_color() 142 elif self._hovered: 143 return Color.from_rgba(*eveThemeColor.THEME_FOCUSDARK).with_alpha(0.6) 144 else: 145 return TextColor.DISABLED 146 147 @staticmethod 148 def _get_handle_highlight_color(): 149 return eveThemeColor.THEME_FOCUS 150 151 def _update_handle_color(self, duration = 0.3): 152 if self._handle: 153 animations.SpColorMorphTo(self._handle, startColor=self._handle.color, endColor=self._get_handle_color(), duration=duration) 154 155 def _highlight_scroll_handle(self): 156 self._handle_highlight_timestamp = gametime.now() 157 if self._handle_highlight_tasklet is None: 158 self._handle_highlight_tasklet = uthread2.start_tasklet(self._do_highlight_handle) 159 160 def _do_highlight_handle(self): 161 try: 162 self._handle_color_override = self._get_handle_highlight_color() 163 self._update_handle_color(duration=0.1) 164 while True: 165 remaining = (self._handle_highlight_timestamp + self.HANDLE_HIGHLIGHT_DURATION - gametime.now()).total_seconds() 166 if remaining > 0.0: 167 uthread2.sleep(remaining) 168 else: 169 self._handle_color_override = None 170 self._update_handle_color(duration=0.5) 171 break 172 173 finally: 174 self._handle_highlight_tasklet = None 175 176 def _get_handle_size_min(self): 177 if self._axis == Axis.HORIZONTAL: 178 return (self.HANDLE_SIZE_MIN, None) 179 if self._axis == Axis.VERTICAL: 180 return (None, self.HANDLE_SIZE_MIN) 181 182 def _update_handle_size_min(self): 183 if self._handle: 184 self._handle.minWidth, self._handle.minHeight = self._get_handle_size_min() 185 186 def _get_current_handle_length(self): 187 if self._axis == Axis.HORIZONTAL: 188 return reverse_scale_dpi(self._handle.displayWidth) 189 if self._axis == Axis.VERTICAL: 190 return reverse_scale_dpi(self._handle.displayHeight) 191 192 def _get_current_track_length(self): 193 if self._axis == Axis.HORIZONTAL: 194 return reverse_scale_dpi(self.displayWidth) 195 if self._axis == Axis.VERTICAL: 196 return reverse_scale_dpi(self.displayHeight) 197 198 def _get_handle_size_and_position(self): 199 if self._handle: 200 track_length = self._get_current_track_length() 201 handle_length = self._get_current_handle_length() 202 if mathext.is_almost_zero(track_length): 203 handle_size_fraction = 1.0 204 else: 205 handle_size_fraction = handle_length / float(track_length) 206 position = mathext.clamp(self._scroll_fraction * (1.0 - handle_size_fraction), low=0.0, high=1.0) 207 if self._axis == Axis.HORIZONTAL: 208 return (self._handle_size_fraction, 0, position) 209 if self._axis == Axis.VERTICAL: 210 return (0, self._handle_size_fraction, position) 211 else: 212 return (0, 0, 0) 213 214 def _update_handle_size_and_position(self): 215 if self._handle: 216 self._handle.width, self._handle.height, position = self._get_handle_size_and_position() 217 if self._axis == Axis.HORIZONTAL: 218 self._handle.left = position 219 elif self._axis == Axis.VERTICAL: 220 self._handle.top = position 221 222 def _get_handle_expanded(self): 223 return self._hovered 224 225 def _update_handle_expanded(self): 226 if self._handle: 227 self._handle.expanded = self._get_handle_expanded() 228 229 def _on_handle_scroll_move(self, delta): 230 scrollable_length = self._get_scrollable_track_length() 231 if mathext.is_almost_zero(scrollable_length): 232 scroll_fraction = 0.0 233 else: 234 scroll_fraction = delta / float(scrollable_length) 235 self.scroll_fraction += scroll_fraction 236 237 def _get_scrollable_track_length(self): 238 if self._handle: 239 return self._get_current_track_length() - self._get_current_handle_length() 240 else: 241 return 0 242 243 def _on_handle_hovered_changed(self, handle): 244 self._update_handle_color(duration=0.1) 245 246 def _get_track_opacity(self): 247 if self._active or self._hovered: 248 return self.TRACK_OPACITY_ACTIVE 249 else: 250 return self.TRACK_OPACITY_INACTIVE 251 252 def _update_track_opacity(self): 253 if self._track: 254 animations.FadeTo(self._track, startVal=self._track.opacity, endVal=self._get_track_opacity(), duration=0.3) 255 256 def _get_track_fraction_at_position(self, x, y): 257 track_length = self._get_current_track_length() 258 if self._axis == Axis.VERTICAL: 259 mouse_position = y 260 _, track_start = self.GetCurrentAbsolutePosition() 261 else: 262 mouse_position = x 263 track_start, _ = self.GetCurrentAbsolutePosition() 264 return mathext.clamp((mouse_position - track_start) / float(track_length), low=0.0, high=1.0) 265 266 def _get_scroll_fraction_at_position(self, x, y): 267 track_length = self._get_current_track_length() 268 if self._axis == Axis.VERTICAL: 269 mouse_position = y 270 _, track_start = self.GetCurrentAbsolutePosition() 271 else: 272 mouse_position = x 273 track_start, _ = self.GetCurrentAbsolutePosition() 274 handle_length = self._get_current_handle_length() 275 return mathext.clamp((mouse_position - track_start) / float(track_length - handle_length), low=0.0, high=1.0) 276 277 def _start_page_scrolling(self, target_fraction): 278 if target_fraction >= self.scroll_fraction: 279 page_count = 1 280 else: 281 page_count = -1 282 self.scroll_by_page(page_count) 283 self._start_page_scrolling_tasklet(page_count) 284 285 def _start_page_scrolling_tasklet(self, page_count): 286 self._cancel_page_scrolling_maybe() 287 self._page_scrolling_tasklet = uthread2.start_tasklet(self._page_scrolling_loop, page_count) 288 289 def _cancel_page_scrolling_maybe(self): 290 if self._page_scrolling_tasklet is not None: 291 self._page_scrolling_tasklet.kill() 292 self._page_scrolling_tasklet = None 293 294 def _page_scrolling_loop(self, page_count): 295 try: 296 uthread2.sleep(self.PAGE_SCROLLING_INITIAL_DELAY) 297 while True: 298 if not uicore.uilib.leftbtn: 299 break 300 if self._axis == Axis.VERTICAL: 301 handle_start = self._handle.top 302 else: 303 handle_start = self._handle.left 304 handle_end = handle_start + self._handle_size_fraction 305 target_fraction = self._get_track_fraction_at_position(x=uicore.uilib.x, y=uicore.uilib.y) 306 if page_count > 0 and target_fraction >= handle_end or page_count < 0 and target_fraction <= handle_start: 307 self.scroll_by_page(page_count) 308 uthread2.sleep(self.PAGE_SCROLLING_INTERVAL) 309 310 finally: 311 self._page_scrolling_tasklet = None 312 313 def UpdateAlignment(self, budgetLeft = 0, budgetTop = 0, budgetWidth = 0, budgetHeight = 0, updateChildrenOnly = False): 314 self._update_dimensions() 315 result = super(Scrollbar, self).UpdateAlignment(budgetLeft, budgetTop, budgetWidth, budgetHeight, updateChildrenOnly) 316 self._update_handle_size_and_position() 317 if self._handle._alignmentDirty: 318 super(Scrollbar, self).UpdateAlignment(budgetLeft, budgetTop, budgetWidth, budgetHeight, updateChildrenOnly) 319 return result 320 321 def OnWindowAboveSetActive(self): 322 self._active = True 323 self._update_track_opacity() 324 self._update_handle_color() 325 326 def OnWindowAboveSetInactive(self): 327 self._active = False 328 self._update_track_opacity() 329 self._update_handle_color() 330 331 def OnMouseEnter(self, *args): 332 self._hovered = True 333 PlaySound(uiconst.SOUND_BUTTON_HOVER) 334 self._update_handle_color(duration=0.1) 335 self._update_handle_expanded() 336 self._update_track_opacity() 337 338 def OnMouseExit(self, *args): 339 self._hovered = False 340 self._update_handle_color() 341 self._update_handle_expanded() 342 self._update_track_opacity() 343 344 def OnMouseDown(self, button, *args): 345 if button == uiconst.MOUSELEFT: 346 self._start_page_scrolling(target_fraction=self._get_scroll_fraction_at_position(x=uicore.uilib.x, y=uicore.uilib.y)) 347 348 def OnMouseUp(self, button, *args): 349 if button == uiconst.MOUSELEFT: 350 self._cancel_page_scrolling_maybe() 351 352 353 class ScrollHandle(Container): 354 _fill = None 355 _fill_expanded = None 356 _hovered = False 357 _on_hovered_changed = None 358 _scrolling = False 359 _last_mouse_position = None 360 _on_scroll_move = None 361 362 def __init__(self, axis, color = None, expanded = False, on_hovered_changed = None, on_scroll_move = None, state = uiconst.UI_NORMAL, **kwargs): 363 if color is None: 364 color = eveColor.WHITE 365 color = Color.from_any(color) 366 self._axis = axis 367 self._expanded = expanded 368 self._color = color 369 super(ScrollHandle, self).__init__(state=state, **kwargs) 370 self.opacity = color.opacity 371 self._layout() 372 if on_hovered_changed is not None: 373 self.on_hovered_changed.connect(on_hovered_changed) 374 if on_scroll_move is not None: 375 self.on_scroll_move.connect(on_scroll_move) 376 377 @property 378 def color(self): 379 return self._color.with_alpha(self.opacity) 380 381 @color.setter 382 def color(self, value): 383 self._color = Color.from_any(value) 384 self.opacity = self._color.opacity 385 if self._fill: 386 self._fill.color = self._color.rgb 387 if self._fill_expanded: 388 self._fill_expanded.color = self._color.rgb 389 390 @property 391 def expanded(self): 392 return self._expanded 393 394 @expanded.setter 395 def expanded(self, value): 396 if self._expanded != value: 397 self._expanded = value 398 self._update_expanded() 399 400 @property 401 def hovered(self): 402 return self._hovered 403 404 @property 405 def on_hovered_changed(self): 406 if self._on_hovered_changed is None: 407 self._on_hovered_changed = signals.Signal('{}.on_hovered_changed'.format(self.__class__.__name__)) 408 return self._on_hovered_changed 409 410 @property 411 def scrolling(self): 412 return self._scrolling 413 414 @property 415 def on_scroll_move(self): 416 if self._on_scroll_move is None: 417 self._on_scroll_move = signals.Signal('{}.on_scroll_move'.format(self.__class__.__name__)) 418 return self._on_scroll_move 419 420 def _layout(self): 421 self._fill = Fill(parent=self, align=uiconst.TOALL, padding=self._get_fill_padding(expanded=False), color=self._color.rgb) 422 self._fill_expanded = Fill(parent=self, align=uiconst.TOALL, padding=self._get_fill_padding(expanded=True), color=self._color.rgb, opacity=1.0 if self._expanded else 0.0, outputMode=uiconst.OUTPUT_COLOR_AND_GLOW, glowBrightness=0.3) 423 424 @staticmethod 425 def _get_fill_padding(expanded): 426 if expanded: 427 handle_thickness = Scrollbar.HANDLE_THICKNESS_HOVER 428 else: 429 handle_thickness = Scrollbar.HANDLE_THICKNESS_IDLE 430 pad = int(round((Scrollbar.TRACK_SIZE - handle_thickness) / 2.0)) 431 return (pad, 432 pad, 433 pad, 434 pad) 435 436 def _update_expanded(self): 437 if self._fill_expanded: 438 if self._expanded: 439 animations.FadeIn(self._fill_expanded, duration=0.2) 440 else: 441 animations.FadeOut(self._fill_expanded, duration=0.2) 442 443 def _start_scrolling(self): 444 if not self._scrolling: 445 self._scrolling = True 446 if self._axis == Axis.HORIZONTAL: 447 self._last_mouse_position = uicore.uilib.x 448 elif self._axis == Axis.VERTICAL: 449 self._last_mouse_position = uicore.uilib.y 450 451 def _update_scrolling(self): 452 current_position = 0 453 if self._axis == Axis.HORIZONTAL: 454 current_position = uicore.uilib.x 455 elif self._axis == Axis.VERTICAL: 456 current_position = uicore.uilib.y 457 delta = current_position - self._last_mouse_position 458 if delta != 0 and self._on_scroll_move is not None: 459 self._on_scroll_move(delta) 460 self._last_mouse_position = current_position 461 462 def _stop_scrolling(self): 463 if self._scrolling: 464 self._scrolling = False 465 466 def OnMouseEnter(self, *args): 467 self._hovered = True 468 if self._on_hovered_changed: 469 self._on_hovered_changed(self) 470 471 def OnMouseExit(self, *args): 472 self._hovered = False 473 if self._on_hovered_changed: 474 self._on_hovered_changed(self) 475 476 def OnMouseDown(self, button, *args): 477 if button == uiconst.MOUSELEFT: 478 self._start_scrolling() 479 480 def OnMouseMove(self, *args): 481 if self._scrolling: 482 self._update_scrolling() 483 484 def OnMouseUp(self, button, *args): 485 if button == uiconst.MOUSELEFT: 486 self._stop_scrolling()