scroll.py (79456B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\carbonui\control\scroll.py 2 import contextlib 3 import logging 4 import sys 5 import weakref 6 import blue 7 import localization 8 import mathext 9 import telemetry 10 import uthread 11 import uthread2 12 from carbon.common.script.util.commonutils import StripTags 13 from carbonui import Axis, fontconst, uiconst 14 from carbonui.control.contextMenu.menuUtil import GetContextMenuOwner, HasContextMenu 15 from carbonui.control.label import LabelCore 16 from carbonui.control.scrollbar import Scrollbar 17 from carbonui.control.scrollColumnHeader import ColumnHeader 18 from carbonui.control.scroll_const import SortDirection, GetHiddenColumnsKey 19 from carbonui.control.scrollentries import SE_ListGroupCore 20 from carbonui.decorative.panelUnderlay import PanelUnderlay 21 from carbonui.fontconst import DEFAULT_FONTSIZE, DEFAULT_LETTERSPACE, DEFAULT_UPPERCASE 22 from carbonui.primitives.container import Container 23 from carbonui.primitives.containerAutoSize import ContainerAutoSize 24 from carbonui.primitives.frame import Frame 25 from carbonui.text.color import TextColor 26 from carbonui.uianimations import animations 27 from carbonui.uicore import uicore 28 from carbonui.util.stringManip import GetAsUnicode 29 from carbonui.util.various_unsorted import GetAttrs, GetWindowAbove, SortListOfTuples 30 from carbonui.control.buttonIcon import ButtonIcon 31 from eve.client.script.ui.control.entries.generic import Generic 32 from eve.client.script.ui.control.entries.util import GetFromClass 33 from eve.client.script.ui.control.eveLabel import EveCaptionLarge 34 from eve.client.script.ui.control.eveLoadingWheel import LoadingWheel 35 from player_messaging.client.ui_message import message_player 36 from eve.client.script.ui.control.themeColored import LineThemeColored 37 from eve.common.lib import appConst as const 38 from eveexceptions import ExceptionEater 39 from menu import MenuLabel 40 from signals import Signal 41 log = logging.getLogger(__name__) 42 MINCOLUMNWIDTH = 24 43 VERSION = uiconst.SCROLLVERSION 44 TABMARGIN = uiconst.LABELTABMARGIN 45 MAIN_CONTAINER_PADDING = 0 46 PRIMARY_COLUMN = 'primaryColumn_%s' 47 SMART_SORT_DIRECTION = 'smartSortDirection_%s' 48 49 def SmartCompare(x, y, sortOrder): 50 for column, direction in sortOrder: 51 if direction == SortDirection.ASCENDING: 52 if x[0][column] > y[0][column]: 53 return 1 54 if x[0][column] < y[0][column]: 55 return -1 56 elif direction == SortDirection.DESCENDING: 57 if x[0][column] < y[0][column]: 58 return 1 59 if x[0][column] > y[0][column]: 60 return -1 61 else: 62 raise ValueError('Unknown sort direction') 63 64 return 0 65 66 67 class Scroll(Container): 68 default_name = 'scroll' 69 default_multiSelect = 1 70 default_stickToBottom = 0 71 default_smartSort = 0 72 default_id = None 73 default_state = uiconst.UI_NORMAL 74 default_discardNonVisibleNodes = False 75 default_columnWidth = {} 76 default_hasUnderlay = False 77 default_rowPadding = 0 78 default_shouldAddFinalRowPadding = True 79 default_innerPadding = (0, 0, 0, 0) 80 default_pushContent = True 81 default_cursor = uiconst.UICURSOR_DEFAULT 82 default_headerFontSize = fontconst.EVE_SMALL_FONTSIZE 83 scrollEnabled = True 84 sortGroups = False 85 reverseGroupSortEnabled = False 86 entriesTotalHeight = 0 87 dragHoverScrollSpeed = 1.0 88 dragHoverScrollAreaSize = 30 89 isLinelessScroll = True 90 tabMargin = TABMARGIN 91 _scrollbar_vertical = None 92 _suppress_scroll_vertical_update = False 93 94 def ApplyAttributes(self, attributes): 95 self._innerPadding = self._sanitize_inner_padding_value(attributes.get('innerPadding', self.default_innerPadding)) 96 self._pushContent = attributes.get('pushContent', self.default_pushContent) 97 self.headerFontSize = attributes.get('headerFontSize', self.default_headerFontSize) 98 Container.ApplyAttributes(self, attributes) 99 self.sr.maincontainer = Container(parent=self, name='maincontainer', padding=MAIN_CONTAINER_PADDING, clipChildren=True) 100 self.sr.scrollHeaders = Container(name='scrollHeaders', parent=self.sr.maincontainer, height=18, state=uiconst.UI_HIDDEN, align=uiconst.TOTOP) 101 self.sr.clipper = Container(name='__clipper', align=uiconst.TOALL, parent=self.sr.maincontainer, padding=self._get_clipper_padding(), clipChildren=True) 102 self.sr.clipper._OnSizeChange_NoBlock = self.OnClipperResize 103 self.sr.content = Container(name='__content', align=uiconst.RELATIVE, parent=self.sr.clipper, state=uiconst.UI_NORMAL) 104 self.Release() 105 self.multiSelect = attributes.get('multiSelect', self.default_multiSelect) 106 self.stickToBottom = attributes.get('stickToBottom', self.default_stickToBottom) 107 self.discardNonVisibleNodes = attributes.get('discardNonVisibleNodes', self.default_discardNonVisibleNodes) 108 self.smartSort = attributes.get('smartSort', self.default_smartSort) 109 self.sr.id = attributes.get('id', self.default_id) 110 self.loadingWheel = None 111 self.hasUnderlay = attributes.get('hasUnderlay', self.default_hasUnderlay) 112 self.rowPadding = attributes.get('rowPadding', self.default_rowPadding) 113 self.shouldAddFinalRowPadding = attributes.get('shouldAddFinalRowPadding', self.default_shouldAddFinalRowPadding) 114 self.on_content_resize = Signal('on_content_resize') 115 self.sr.selfProxy = weakref.proxy(self) 116 self.Prepare_() 117 self._mouseHoverCookie = uicore.uilib.RegisterForTriuiEvents(uiconst.UI_MOUSEHOVER, self.OnGlobalMouseHover) 118 119 @property 120 def innerPadding(self): 121 return self._innerPadding 122 123 @innerPadding.setter 124 def innerPadding(self, value): 125 value = self._sanitize_inner_padding_value(value) 126 if self._innerPadding != value: 127 self._innerPadding = value 128 self._update_clipper_padding() 129 130 def _sanitize_inner_padding_value(self, value): 131 if value is None: 132 value = self.default_innerPadding 133 if isinstance(value, int): 134 value = (value, 135 value, 136 value, 137 value) 138 return value 139 140 def _get_clipper_padding(self): 141 inner_pad_left, _, inner_pad_right, _ = self._innerPadding 142 return (inner_pad_left, 143 0, 144 inner_pad_right, 145 0) 146 147 def _update_clipper_padding(self): 148 self.sr.clipper.padding = self._get_clipper_padding() 149 150 def Close(self, *args): 151 with ExceptionEater('Closing a Scroll'): 152 uicore.uilib.UnregisterForTriuiEvents(self._mouseHoverCookie) 153 super(Scroll, self).Close() 154 155 def OnGlobalMouseHover(self, obj, *args): 156 if uicore.IsDragging() and (obj == self or obj.IsUnder(self.sr.content)): 157 l, t, w, h = self.GetAbsolute() 158 if self.IsScrollbarVisible(): 159 y = uicore.uilib.y - t 160 if y <= self.dragHoverScrollAreaSize: 161 self.Scroll(self.dragHoverScrollSpeed) 162 elif y > h - self.dragHoverScrollAreaSize: 163 self.Scroll(-self.dragHoverScrollSpeed) 164 return True 165 166 def IsScrollbarVisible(self): 167 return self._scrollbar_vertical and self._scrollbar_vertical.display 168 169 def Prepare_(self): 170 self.Prepare_Underlay_() 171 self.Prepare_ScrollControls_() 172 173 def Prepare_Underlay_(self): 174 if getattr(self, 'hasUnderlay', self.default_hasUnderlay): 175 self.sr.underlay = PanelUnderlay(parent=self, name='background') 176 177 def Prepare_ScrollControls_(self): 178 self.noResultsContainer = None 179 self.noResultsLabel = None 180 self._scrollbar_vertical = Scrollbar(parent=self.sr.maincontainer, align=uiconst.TORIGHT, axis=Axis.VERTICAL, padLeft=8 if self._pushContent else 0, on_scroll_fraction_changed=self._on_scroll_fraction_vertical_changed, idx=0) 181 self._scrollbar_vertical.display = False 182 183 def _on_scroll_fraction_vertical_changed(self, scrollbar): 184 with self.suppress_scroll_vertical_update(): 185 self.ScrollToProportion(proportion=scrollbar.scroll_fraction, threaded=False) 186 187 @contextlib.contextmanager 188 def suppress_scroll_vertical_update(self): 189 self._suppress_scroll_vertical_update = True 190 try: 191 yield 192 finally: 193 self._suppress_scroll_vertical_update = False 194 195 def RemoveActiveFrame(self, *args): 196 pass 197 198 def HideBackground(self, alwaysHidden = 0): 199 if getattr(self, 'hasUnderlay', self.default_hasUnderlay): 200 self.sr.underlay.state = uiconst.UI_HIDDEN 201 if alwaysHidden: 202 self.SetNoBackgroundFlag(alwaysHidden) 203 204 HideUnderLay = HideBackground 205 206 def SetNoBackgroundFlag(self, hide = 0): 207 self.hideBackground = hide 208 209 def Release(self): 210 self.isTabStop = 1 211 self._loading = 0 212 self._position = 0 213 self._totalHeight = 0 214 self._fixedEntryHeight = None 215 self._customColumnWidths = None 216 self.sr.scaleLine = None 217 self.sr.headers = [] 218 self.sr.widthToHeaders = {} 219 self.sr.tabs = [] 220 self.sr.nodes = [] 221 self.visibleNodes = [] 222 self.sr.overlays = [] 223 self.sr.underlays = [] 224 self.sr.fixedColumns = {} 225 self.stretchLastHeader = False 226 self.sr.maxDefaultColumns = {} 227 self.sr.minColumnWidth = {} 228 self.sr.defaultColumnWidth = self.default_columnWidth 229 self.sr.ignoreTabTrimming = 0 230 self.showColumnLines = True 231 self.sr.id = None 232 self.sortBy = None 233 self.sr.iconMargin = 0 234 self.sr.hint = None 235 self.scrollingRange = 0 236 self.hiliteSorted = 1 237 self.reversedSort = 0 238 self.debug = 0 239 self.scrolling = 0 240 self.scalingcol = 0 241 self.refreshingColumns = 0 242 self.lastDrawnColumns = None 243 self.allowFilterColumns = 0 244 self.lastSelected = None 245 self.lastHeaders = None 246 self.bumpHeaders = 1 247 self.slimHeaders = 0 248 self.newColumns = 0 249 self.trimFast = 0 250 self.refs = [] 251 self.lastCharReceivedAt = 0 252 self.currChars = '' 253 self.hideBackground = False 254 self._ignoreSort = 0 255 self._showCollapseIcon = False 256 self._columnOffset = 0 257 self._pendingPositionUpdate = None 258 self.loopThread = None 259 260 def GetVisibleNodes(self): 261 if self.visibleNodes: 262 return self.visibleNodes 263 return self.sr.nodes 264 265 def HasUnderlay(self): 266 return hasattr(self.sr, 'underlay') and self.sr.underlay is not None 267 268 def OnSetFocus(self, *args): 269 if self and not self.destroyed and self.parent and self.parent.name == 'inlines': 270 if self.parent.parent and self.parent.parent.sr.node: 271 self.parent.parent.sr.node.scroll.ShowNodeIdx(self.parent.parent.sr.node.idx) 272 if self.HasUnderlay() and hasattr(self.sr.underlay, 'AnimEntry'): 273 self.sr.underlay.AnimEntry() 274 275 def OnKillFocus(self, *args): 276 if self.HasUnderlay() and hasattr(self.sr.underlay, 'AnimExit'): 277 self.sr.underlay.AnimExit() 278 279 def _show_header_resizer_hints(self): 280 header_class = self.GetScrollHeaderClass() 281 for child in self.sr.innerScrollHeaders.children: 282 if isinstance(child, header_class): 283 child.show_resizer_hint() 284 285 def _hide_header_resizer_hints(self): 286 header_class = self.GetScrollHeaderClass() 287 for child in self.sr.innerScrollHeaders.children: 288 if isinstance(child, header_class): 289 child.hide_resizer_hint() 290 291 def DrawHeaders(self, headers, tabs = []): 292 self.sr.scrollHeaders.state = uiconst.UI_HIDDEN 293 self.sr.scrollHeaders.Flush() 294 self.sr.innerScrollHeaders = Container(name='innerScrollHeaders', parent=self.sr.scrollHeaders, clipChildren=True) 295 self.sr.innerScrollHeaders.OnMouseEnter = lambda *args: self._show_header_resizer_hints() 296 self.sr.innerScrollHeaders.OnMouseExit = lambda *args: self._hide_header_resizer_hints() 297 for each in self.sr.clipper.children[:]: 298 if each.name == '__columnLine': 299 each.Close() 300 if each.name == '__columnFill': 301 each.Close() 302 303 self.lastDrawnColumns = None 304 self.sr.widthToHeaders = {} 305 if not (headers and tabs and len(headers) == len(tabs)): 306 self.sr.headers = [] 307 self.sr.tabs = [] 308 return 309 if not self.sr.nodes: 310 return 311 self.sr.scrollHeaders.state = uiconst.UI_NORMAL 312 self.sr.scrollHeaders.SetOrder(0) 313 if not self.isLinelessScroll: 314 LineThemeColored(parent=self.sr.scrollHeaders, align=uiconst.TOBOTTOM, opacity=uiconst.OPACITY_FRAME, idx=0) 315 columnOffsetToUse = self.GetEffectiveColumnOffset() 316 if self._showCollapseIcon: 317 self.AddCollapseBtn() 318 elif columnOffsetToUse: 319 self.sr.innerScrollHeaders.padLeft = columnOffsetToUse 320 headerHints = self.GetHeaderHints() 321 i = 0 322 totalWidth = 0 323 maxTextHeight = 0 324 headerClass = self.GetScrollHeaderClass() 325 half_scale_handle_width = int(round(headerClass.SCALE_HANDLE_SIZE / 2.0)) 326 for header in headers: 327 width = self.sr.fixedColumns.get(header, None) 328 if width is None: 329 if len(headers) == 1: 330 width = 128 331 else: 332 width = tabs[i] 333 if i > 0: 334 width = width - tabs[i - 1] 335 if self.smartSort: 336 selected = self.GetPrimaryColumn() == header 337 sortdir = self.GetSmartSortDirection(header) 338 else: 339 selected = self.sortBy == header 340 sortdir = SortDirection.from_legacy_reversed_sort(self.GetSortDirection()) if selected else SortDirection.ASCENDING 341 scalable = self.sr.id and header not in self.sr.fixedColumns and (not self.stretchLastHeader or header != headers[-1]) 342 self.sr.widthToHeaders[header] = totalWidth 343 headerparent = headerClass(column_id=header, name=header, parent=self.sr.innerScrollHeaders, align=uiconst.TOLEFT, left=-half_scale_handle_width if i > 0 else 0, width=width + half_scale_handle_width, tab_margin=self.tabMargin, state=uiconst.UI_NORMAL, text=header, direction=sortdir, selected=selected, on_click=lambda _header = header: self.ChangeSortBy(_header), on_double_click=lambda _header = header: self.ResetColumnWidth(_header), get_menu=lambda _header = header: self.GetHeaderMenu(_header), scalable=scalable, on_scale_start=self._on_column_scale_start, header_hint=headerHints.get(header, None)) 344 headerparent.on_scale_move = lambda column = header, element = headerparent: self._on_column_scale_move(column, element) 345 headerparent.on_scale_end = lambda column = header, element = headerparent: self._on_column_scale_end(column, element) 346 if self.stretchLastHeader and header == headers[-1]: 347 headerparent.align = uiconst.TOALL 348 headerparent.width = 0 349 totalWidth += width 350 if not (self.smartSort or self.allowFilterColumns): 351 headerparent.GetMenu = None 352 maxTextHeight = max(maxTextHeight, headerparent.GetHeaderHeight()) 353 if not self.isLinelessScroll and headerparent.align != uiconst.TOALL and not self.sr.ignoreTabTrimming and self.sr.nodes and self.showColumnLines: 354 self.BuildColumnLine(columnOffsetToUse, totalWidth, width, headerparent) 355 i += 1 356 357 self.sr.scrollHeaders.height = maxTextHeight 358 self.lastDrawnColumns = headers 359 360 def BuildColumnLine(self, columnOffsetToUse, totalWidth, headerWidth, headerparent): 361 line = LineThemeColored(parent=self.sr.clipper, align=uiconst.RELATIVE, name='__columnLine', opacity=uiconst.OPACITY_FRAME) 362 line.width = 1 363 line.height = uicore.desktop.height 364 line.top = 1 365 lineLeft = totalWidth - 1 366 line.originalLeft = lineLeft 367 line.left = lineLeft + columnOffsetToUse 368 369 def BuildColumnFill(self, columnOffsetToUse, totalWidth, headerWidth, headerparent): 370 lineLeft = totalWidth - headerWidth + columnOffsetToUse 371 columnFill = Frame(parent=self.sr.clipper, align=uiconst.RELATIVE, name='__columnFill', opacity=0.1, frameConst=uiconst.FRAME_FILLED_CORNER0, color=(0.94, 0.94, 0.94, 1.0), pos=(lineLeft, 372 0, 373 headerWidth, 374 uicore.desktop.height)) 375 columnFill.originalLeft = lineLeft 376 headerFrame = Frame(parent=headerparent, align=uiconst.TOALL, name='__columnFill', opacity=0.1, frameConst=uiconst.FRAME_FILLED_CORNER5, idx=0, color=(0.94, 0.94, 0.94, 1.0), padBottom=-10) 377 headerparent.sr.headerFrame = headerFrame 378 379 def GetScrollHeaderClass(self): 380 return ColumnHeader 381 382 def UpdateColumnLines(self): 383 columnOffsetToUse = self.GetEffectiveColumnOffset() 384 for each in self.sr.clipper.children: 385 if each.name == '__columnLine': 386 left = getattr(each, 'originalLeft', each.left) 387 each.left = left + columnOffsetToUse 388 elif each.name == '__columnFill': 389 left = getattr(each, 'originalLeft', each.left) 390 each.left = left + columnOffsetToUse 391 392 def ShowHint(self, hint = None, labelClass = EveCaptionLarge): 393 if not hint: 394 if self.noResultsContainer: 395 self.noResultsContainer.Hide() 396 return 397 center_hint = '<center>%s</center>' % hint 398 isNew = self.noResultsLabel is None or self.noResultsLabel.GetText() != center_hint 399 if self.noResultsContainer is None: 400 self.ConstructNoResultsContainer(center_hint, labelClass) 401 self.noResultsLabel.SetText(center_hint) 402 self.noResultsContainer.SetState(uiconst.UI_DISABLED) 403 if isNew: 404 animations.FadeTo(self.noResultsContainer, 0.0, 1.0, duration=0.3) 405 406 def ConstructNoResultsContainer(self, center_hint, labelClass): 407 self.noResultsContainer = Container(name='noResultsContainer', parent=self.sr.clipper, align=uiconst.TOALL) 408 cont = ContainerAutoSize(name='cont', parent=self.noResultsContainer, align=uiconst.TOTOP) 409 self.noResultsLabel = labelClass(name='noResultsLabel', parent=cont, align=uiconst.TOTOP, text=center_hint, autoFitToText=True, padding=16, color=TextColor.SECONDARY) 410 411 def _on_column_scale_start(self): 412 self.scalingcol = uicore.uilib.x 413 if self.sr.scaleLine is not None: 414 self.sr.scaleLine.Close() 415 self.ContructScaleLine() 416 417 def ContructScaleLine(self): 418 l, t, w, h = self.GetAbsolute() 419 self.sr.scaleLine = LineThemeColored(parent=self, align=uiconst.TOPLEFT, pos=(uicore.uilib.x - l - 1, 420 1, 421 2, 422 h), idx=0, opacity=uiconst.OPACITY_FRAME) 423 424 def _on_column_scale_move(self, column, element): 425 l, t, w, h = self.GetAbsolute() 426 element_left, _ = element.GetCurrentAbsolutePosition() 427 minColumnWidth = self.sr.minColumnWidth.get(column, MINCOLUMNWIDTH) 428 if self.scalingcol and self.sr.scaleLine: 429 self.sr.scaleLine.left = mathext.clamp(value=uicore.uilib.x - l - 2, low=element_left - l + minColumnWidth, high=w - minColumnWidth) 430 431 def _on_column_scale_end(self, column, element): 432 if self.sr.scaleLine is not None: 433 if self.sr.id and self.scalingcol != uicore.uilib.x: 434 currentSettings = settings.user.ui.Get('columnWidths_%s' % VERSION, {}) 435 currentSettings.setdefault(self.sr.id, {}) 436 currentWidth = element.width 437 minColumnWidth = self.sr.minColumnWidth.get(column, MINCOLUMNWIDTH) 438 if self.sr.scaleLine: 439 l, t, w, h = self.GetAbsolute() 440 newLeft = self.sr.scaleLine.left + l 441 else: 442 newLeft = uicore.uilib.x 443 diff = newLeft - self.scalingcol 444 newWidth = currentWidth + diff 445 currentSettings[self.sr.id][column] = max(minColumnWidth, newWidth) 446 settings.user.ui.Set('columnWidths_%s' % VERSION, currentSettings) 447 uthread.pool('VirtualScroll::EndScaleCol-->UpdateTabStops', self.UpdateTabStops, 'EndScaleCol') 448 scaleLine = self.sr.scaleLine 449 self.sr.scaleLine = None 450 scaleLine.Close() 451 self.scalingcol = 0 452 453 def OnColumnChanged(self, *args): 454 pass 455 456 def OnNewHeaders(self, *args): 457 pass 458 459 def ShowNodeIdx(self, idx, threadedScrolling = True): 460 if self.scrollingRange: 461 node = self.GetNode(idx) 462 fromTop = node.scroll_positionFromTop 463 if fromTop is None: 464 return 465 if self._position > fromTop: 466 portion = fromTop / float(self.scrollingRange) 467 self.ScrollToProportion(portion, threaded=threadedScrolling) 468 clipperWidth, clipperHeight = self.GetContentParentSize() 469 nodeHeight = self.GetNodeHeight(node, clipperWidth) 470 if self._position + clipperHeight < fromTop + nodeHeight: 471 portion = (fromTop - clipperHeight + nodeHeight) / float(self.scrollingRange) 472 self.ScrollToProportion(portion, threaded=threadedScrolling) 473 474 def GetNodes(self, allowNone = False): 475 ret = [] 476 for each in self.sr.nodes: 477 if each.internalNodes: 478 if allowNone: 479 ret += each.internalNodes 480 else: 481 for internal in each.internalNodes: 482 if internal: 483 ret.append(internal) 484 485 else: 486 ret.append(each) 487 488 return ret 489 490 def SetSelected(self, idx): 491 node = self.GetNode(idx) 492 if node: 493 self.SelectNode(node) 494 self.ReportSelectionChange() 495 496 def ActivateIdx(self, idx): 497 node = self.GetNode(min(idx, len(self.GetNodes()) - 1)) 498 if node: 499 self.SelectNode(node) 500 self.ShowNodeIdx(node.idx) 501 else: 502 self.ReportSelectionChange() 503 504 def _SelectNode(self, node): 505 if getattr(node, 'selectable', 1) == 0: 506 return 507 node.selected = 1 508 self.UpdateSelection(node) 509 510 def _DeselectNode(self, node): 511 node.selected = 0 512 self.UpdateSelection(node) 513 514 def SelectNodes(self, nodeList): 515 self.DeselectAll() 516 for node in nodeList: 517 self._SelectNode(node) 518 519 self.ReportSelectionChange() 520 521 def SelectNode(self, node, multi = 0, subnode = None, checktoggle = 1): 522 control = uicore.uilib.Key(uiconst.VK_CONTROL) 523 shift = uicore.uilib.Key(uiconst.VK_SHIFT) 524 selected = node.get('selected', 0) 525 if not self.multiSelect: 526 self.DeselectAll(0) 527 if control: 528 if not selected: 529 self._SelectNode(node) 530 else: 531 self._SelectNode(node) 532 elif not control and not shift: 533 self.DeselectAll(0) 534 self._SelectNode(node) 535 elif control and not shift: 536 if not selected: 537 self._SelectNode(node) 538 else: 539 self._DeselectNode(node) 540 elif not control and shift: 541 if self.lastSelected is not None and self.lastSelected != node.idx: 542 self.DeselectAll(0) 543 r = [self.lastSelected, node.idx] 544 r.sort() 545 for i in xrange(r[0], r[1] + 1): 546 _node = self.GetNode(i, checkInternalNodes=True) 547 if _node: 548 self._SelectNode(_node) 549 550 self.ReportSelectionChange() 551 return 552 self.DeselectAll(0) 553 self._SelectNode(node) 554 elif control and shift: 555 if self.lastSelected is not None and self.lastSelected != node.idx: 556 r = [self.lastSelected, node.idx] 557 r.sort() 558 for i in xrange(r[0], r[1] + 1): 559 _node = self.GetNode(i, checkInternalNodes=True) 560 if _node: 561 self._SelectNode(_node) 562 563 else: 564 self.DeselectAll(0) 565 self._SelectNode(node) 566 self.lastSelected = node.idx 567 self.ReportSelectionChange() 568 569 def ReportSelectionChange(self): 570 self.OnSelectionChange(self.GetSelected()) 571 572 def OnSelectionChange(self, *args): 573 pass 574 575 def DeselectAll(self, report = 1, *args): 576 for node in self.GetNodes(): 577 node.selected = 0 578 self.UpdateSelection(node) 579 580 if report: 581 self.ReportSelectionChange() 582 583 def SelectAll(self, *args): 584 if not self.multiSelect: 585 return 586 for node in self.GetNodes(): 587 if getattr(node, 'selectable', 1) == 0: 588 continue 589 node.selected = 1 590 self.UpdateSelection(node) 591 592 self.ReportSelectionChange() 593 594 def ToggleSelected(self, *args): 595 for node in self.GetNodes(): 596 node.selected = not node.get('selected', 0) 597 self.UpdateSelection(node) 598 599 self.ReportSelectionChange() 600 601 def UpdateSelection(self, node): 602 if node.panel: 603 if node.panel.sr.selection: 604 node.panel.sr.selection.state = [uiconst.UI_HIDDEN, uiconst.UI_DISABLED][node.selected] 605 elif node.selected and hasattr(node.panel, 'Select'): 606 node.panel.Select() 607 elif not node.selected and hasattr(node.panel, 'Deselect'): 608 node.panel.Deselect() 609 610 def ClearSelection(self, *args): 611 for node in self.GetNodes(): 612 node.selected = 0 613 self.UpdateSelection(node) 614 615 self.lastSelected = None 616 self.ReportSelectionChange() 617 618 def GetSelectedNodes(self, node, toggle = 0): 619 if not node.get('selected', 0) or toggle: 620 self.SelectNode(node) 621 sel = [] 622 for each in self.GetNodes(): 623 if each.get('selected', 0): 624 sel.append(each) 625 626 return sel 627 628 def GetSelected(self): 629 sel = [] 630 for each in self.GetNodes(): 631 if each.get('selected', 0): 632 sel.append(each) 633 634 return sel 635 636 def GetSortBy(self): 637 if self.smartSort: 638 return None 639 if self.sr.id: 640 pr = settings.user.ui.Get('scrollsortby_%s' % VERSION, {}) 641 if self.sr.id in pr: 642 return pr[self.sr.id][0] 643 return self.sortBy 644 645 def GetSortDirection(self): 646 if self.sr.id: 647 pr = settings.user.ui.Get('scrollsortby_%s' % VERSION, {}) 648 if self.sr.id in pr: 649 return pr[self.sr.id][1] 650 return self.reversedSort 651 652 def GetSmartSortDirection(self, column, default = SortDirection.ASCENDING): 653 if self.sr.id and self.smartSort: 654 pr = settings.user.ui.Get(SMART_SORT_DIRECTION % VERSION, {}) 655 if self.sr.id in pr: 656 return pr[self.sr.id].get(column, default) 657 return default 658 659 def ToggleSmartSortDirection(self, column): 660 if self.sr.id and self.smartSort: 661 direction_current = self.GetSmartSortDirection(column) 662 direction_new = SortDirection.flip(direction_current) 663 pr = settings.user.ui.Get(SMART_SORT_DIRECTION % VERSION, {}) 664 if self.sr.id not in pr: 665 pr[self.sr.id] = {} 666 pr[self.sr.id][column] = direction_new 667 settings.user.ui.Set(SMART_SORT_DIRECTION % VERSION, pr) 668 669 def GetSortValue(self, by, node, idx = None): 670 if getattr(node, 'sortValues', None): 671 return node.sortValues[idx] 672 if getattr(node, 'GetSortValue', None): 673 return node.GetSortValue(node, by, self.GetSortDirection(), idx=idx) 674 ret = self._GetSortValue(by, node, idx) 675 return StripTags(ret, stripOnly=['localized']) 676 677 def _GetSortValue(self, by, node, idx): 678 val = node.Get('sort_' + by, None) 679 if val is None: 680 val = node.Get('sort_' + by.replace('<br>', ' '), None) 681 if val is not None: 682 try: 683 val = val.lower() 684 except: 685 sys.exc_clear() 686 687 return val 688 if idx is not None: 689 strings = self.GetStringFromNode(node).split('<t>') 690 if len(strings) > idx: 691 value = strings[idx].lower() 692 try: 693 value = uicore.font.DeTag(value) 694 isAU = value.find('au') != -1 695 isKM = value.find('km') != -1 696 value = float(value.replace('m\xef\xbf\xbd', '').replace('isk', '').replace('km', '').replace('au', '').replace(',', '').replace(' ', '').replace('m3', '')) 697 if isAU: 698 value *= const.AU 699 elif isKM: 700 value *= 1000 701 return value 702 except Exception: 703 sys.exc_clear() 704 rest = ''.join(strings[idx + 1:]) 705 return value + rest 706 707 return 'aaa' 708 val = node.Get(by, '-') 709 try: 710 val = val.lower() 711 except: 712 sys.exc_clear() 713 714 return val 715 716 def GetContentContainer(self): 717 return self.sr.content 718 719 def GetColumns(self): 720 if self.sr.id and (self.smartSort or self.allowFilterColumns): 721 if not self.sr.headers: 722 return [] 723 orderedColumns = settings.user.ui.Get('columnOrder_%s' % VERSION, {}).get(self.sr.id, self.sr.headers) 724 notInOrdered = [ header for header in self.sr.headers if header not in orderedColumns ] 725 headers = [ header for header in orderedColumns + notInOrdered if header in self.sr.headers ] 726 hiddenColumnsKey = GetHiddenColumnsKey(self.sr.id) 727 hiddenColumns = settings.user.ui.Get('filteredColumns_%s' % VERSION, {}).get(hiddenColumnsKey, []) 728 allHiddenColumns = hiddenColumns + settings.user.ui.Get('filteredColumnsByDefault_%s' % VERSION, {}).get((hiddenColumnsKey, session.languageID), []) 729 filterColumns = filter(lambda x: x not in allHiddenColumns, headers) 730 return filterColumns 731 else: 732 return self.sr.headers 733 734 def GetHeaderMenu(self, label): 735 m = [] 736 if self.smartSort: 737 m += [(MenuLabel('/Carbon/UI/Commands/CmdMakePrimary'), self.MakePrimary, (label,))] 738 if self.smartSort or self.allowFilterColumns: 739 if len(self.GetColumns()) > 1: 740 m += [(MenuLabel('/Carbon/UI/Common/Hide', {'label': label}), self.HideColumn, (label,))] 741 m += self.GetShowColumnMenu() 742 return m 743 744 def GetHeaderHints(self): 745 return {} 746 747 def GetShowColumnMenu(self): 748 m = [] 749 for label in self.sr.headers: 750 if label not in self.GetColumns(): 751 m.append((MenuLabel('/Carbon/UI/Common/Show', {'label': label}), self.ShowColumn, (label,))) 752 753 if m: 754 m.insert(0, None) 755 return m 756 757 def MakePrimary(self, label, update = 1): 758 all = settings.user.ui.Get(PRIMARY_COLUMN % VERSION, {}) 759 all[self.sr.id] = label 760 settings.user.ui.Set(PRIMARY_COLUMN % VERSION, all) 761 if update: 762 self.ChangeColumnOrder(label, 0) 763 764 def GetPrimaryColumn(self): 765 return settings.user.ui.Get(PRIMARY_COLUMN % VERSION, {}).get(self.sr.id, None) 766 767 def SetColumnsHiddenByDefault(self, columns, *args): 768 if self.sr.id: 769 filteredByDefault = settings.user.ui.Get('filteredColumnsByDefault_%s' % VERSION, {}) 770 key = GetHiddenColumnsKey(self.sr.id) 771 idWithLanguage = (key, session.languageID) 772 if idWithLanguage not in filteredByDefault: 773 filteredByDefault[idWithLanguage] = columns 774 settings.user.ui.Set('filteredColumnsByDefault_%s' % VERSION, filteredByDefault) 775 776 def HideColumn(self, label): 777 if self.sr.id: 778 filtered = settings.user.ui.Get('filteredColumns_%s' % VERSION, {}) 779 key = GetHiddenColumnsKey(self.sr.id) 780 if key not in filtered: 781 filtered[key] = [] 782 if label not in filtered[key]: 783 filtered[key].append(label) 784 settings.user.ui.Set('filteredColumns_%s' % VERSION, filtered) 785 self.OnColumnChanged(None) 786 self.OnNewHeaders() 787 788 def ShowColumn(self, label): 789 if self.sr.id: 790 key = GetHiddenColumnsKey(self.sr.id) 791 filtered = settings.user.ui.Get('filteredColumns_%s' % VERSION, {}) 792 if key in filtered and label in filtered[key]: 793 filtered[key].remove(label) 794 filteredByDefault = settings.user.ui.Get('filteredColumnsByDefault_%s' % VERSION, {}) 795 idWithLanguage = (key, session.languageID) 796 if idWithLanguage in filteredByDefault and label in filteredByDefault[idWithLanguage]: 797 filteredByDefault[idWithLanguage].remove(label) 798 settings.user.ui.Set('filteredColumnsByDefault_%s' % VERSION, filteredByDefault) 799 settings.user.ui.Set('filteredColumns_%s' % VERSION, filtered) 800 self.OnColumnChanged(None) 801 self.OnNewHeaders() 802 803 @telemetry.ZONE_METHOD 804 def Sort(self, by = None, reversesort = 0, forceHilite = 0): 805 if self.smartSort: 806 self._SmartSort() 807 else: 808 self._Sort(by, reversesort) 809 if self.sortBy != by or forceHilite: 810 self.HiliteSorted(by, reversesort) 811 self.sortBy = by 812 813 def _Sort(self, by = None, reversesort = 0): 814 idx = None 815 headers = self.GetColumns() 816 if by in headers: 817 idx = headers.index(by) 818 endOrder = [] 819 self.SortAsRoot(self.sr.nodes, endOrder, by, idx, reversesort) 820 self.SetNodes(endOrder) 821 self.UpdatePosition(fromWhere='Sort') 822 823 def _SmartSort(self): 824 columns = self.GetColumns() 825 primary = self.GetPrimaryColumn() 826 sortcolumns = columns[:] 827 if primary in columns: 828 idx = columns.index(primary) 829 sortcolumns = columns[idx:] 830 if columns: 831 sortData = [] 832 rm = [] 833 for node in self.sr.nodes: 834 nodeData = [] 835 idx = 0 836 for header in columns: 837 if header not in sortcolumns: 838 continue 839 if idx in rm: 840 value = 0 841 else: 842 value = node.Get('sort_%s' % header, None) 843 if value is None: 844 log.warning('Cannot find sortvalue for column %s in scroll %s', header, self.sr.id) 845 rm.append(idx) 846 value = 0 847 else: 848 try: 849 value = value.lower() 850 except: 851 sys.exc_clear() 852 853 idx += 1 854 nodeData.append(value) 855 856 sortData.append([nodeData, node]) 857 858 sortOrder = [ (idx, self.GetSmartSortDirection(header)) for idx, header in enumerate(sortcolumns) if idx not in rm ] 859 sortData.sort(lambda x, y, sortOrder = sortOrder: SmartCompare(x, y, sortOrder)) 860 self.SetNodes([ each[1] for each in sortData ]) 861 self.UpdatePositionThreaded(fromWhere='Sort(Smart)') 862 863 @telemetry.ZONE_METHOD 864 def SortAsRoot(self, nodes, endOrder, columnName, columnIndex, reversedSorting = 0, groupIndex = None): 865 groups = [] 866 rootSortList_Groups = [] 867 rootSortList_NotGroups = [] 868 for node in nodes: 869 if groupIndex is None and node.isSub: 870 continue 871 val = self.GetSortValue(columnName, node, columnIndex) 872 val = (val, self.GetStringFromNode(node).lower()) 873 if issubclass(node.decoClass, SE_ListGroupCore) or getattr(node, 'isListEntry', False): 874 rootSortList_Groups.append((val, node)) 875 else: 876 rootSortList_NotGroups.append((val, node)) 877 878 if self.sortGroups: 879 rootSortList_Groups = SortListOfTuples(rootSortList_Groups) 880 else: 881 rootSortList_Groups = [ node for val, node in rootSortList_Groups ] 882 rootSortList_NotGroups = SortListOfTuples(rootSortList_NotGroups) 883 if reversedSorting: 884 rootSortList_NotGroups.reverse() 885 if self.reverseGroupSortEnabled: 886 rootSortList_Groups.reverse() 887 combinedGroupsAndOthers = rootSortList_Groups + rootSortList_NotGroups 888 if groupIndex is not None: 889 for subIndex, subNode in enumerate(combinedGroupsAndOthers): 890 endOrder.insert(groupIndex + subIndex + 1, subNode) 891 892 else: 893 endOrder.extend(combinedGroupsAndOthers) 894 if rootSortList_Groups: 895 for groupNode in rootSortList_Groups: 896 groupIdx = endOrder.index(groupNode) 897 subNodes = groupNode.get('subNodes', []) 898 self.SortAsRoot(subNodes, endOrder, columnName, columnIndex, reversedSorting, groupIndex=groupIdx) 899 900 return nodes 901 902 def GetStringFromNode(self, node): 903 label_or_text = node.get('label', '') or node.get('text', '') 904 return GetAsUnicode(label_or_text) 905 906 def RefreshSort(self, forceHilite = 0): 907 if self.smartSort: 908 self.Sort() 909 else: 910 sortby = self.GetSortBy() 911 if sortby is not None: 912 self.Sort(sortby, self.GetSortDirection(), forceHilite=forceHilite) 913 914 def ChangeSortBy(self, by, *args): 915 if self.smartSort: 916 old_primary = self.GetPrimaryColumn() 917 if old_primary == by: 918 self.ToggleSmartSortDirection(by) 919 self.MakePrimary(by, 0) 920 for header in self.GetHeadersChildren(): 921 if not self.IsExepectedColumnHeader(header): 922 continue 923 if header.id == by: 924 header.select(direction=self.GetSmartSortDirection(header.id)) 925 else: 926 header.deselect() 927 928 self.Sort() 929 else: 930 if self.sortBy == by: 931 self.reversedSort = not self.reversedSort 932 else: 933 self.reversedSort = False 934 self.sortBy = by 935 if self.sr.id: 936 pr = settings.user.ui.Get('scrollsortby_%s' % VERSION, {}) 937 pr[self.sr.id] = (self.sortBy, self.reversedSort) 938 settings.user.ui.Set('scrollsortby_%s' % VERSION, pr) 939 self.RefreshSort(1) 940 941 def ChangeColumnOrder(self, column, toIdx): 942 if self.sr.id and self.smartSort: 943 all = settings.user.ui.Get('columnOrder_%s' % VERSION, {}) 944 currentOrder = all.get(self.sr.id, self.sr.headers)[:] 945 if column in currentOrder: 946 currentOrder.remove(column) 947 currentOrder.insert(toIdx, column) 948 all[self.sr.id] = currentOrder 949 settings.user.ui.Set('columnOrder_%s' % VERSION, all) 950 self.OnColumnChanged(None) 951 self.OnNewHeaders() 952 953 def HiliteSorted(self, by, rev, *args): 954 selectedHeaderIdx = None 955 totalWidth = 0 956 for i, header in enumerate(self.GetHeadersChildren()): 957 if not self.IsExepectedColumnHeader(header): 958 continue 959 header.deselect() 960 header.direction = SortDirection.ASCENDING 961 if self.hiliteSorted and header.id == by: 962 header.select(direction=SortDirection.from_legacy_reversed_sort(rev)) 963 selectedHeaderIdx = i 964 totalWidth += header.width 965 966 def IsExepectedColumnHeader(self, header): 967 return isinstance(header, ColumnHeader) 968 969 def GetHeadersChildren(self): 970 if self.sr.innerScrollHeaders and not self.sr.innerScrollHeaders.destroyed: 971 return self.sr.innerScrollHeaders.children 972 return [] 973 974 def Clear(self): 975 self.visibleNodes = [] 976 self.LoadContent() 977 978 def ReloadNodes(self, *args, **kwargs): 979 for node in self.GetNodes(): 980 self.PrepareSubContent(node, threadedUpdate=False) 981 if node.panel: 982 node.panel.Load(node) 983 984 @telemetry.ZONE_METHOD 985 def LoadContent(self, fixedEntryHeight = None, contentList = [], sortby = None, reversesort = None, headers = [], scrollTo = None, customColumnWidths = False, showScrollTop = False, noContentHint = '', ignoreSort = False, scrolltotop = False, keepPosition = False, showCollapseIcon = False): 986 if self.destroyed: 987 return 988 if scrolltotop: 989 scrollTo = 0.0 990 elif scrollTo is None or keepPosition: 991 scrollTo = self.GetScrollProportion() 992 self._loading = 1 993 self._fixedEntryHeight = fixedEntryHeight 994 self._customColumnWidths = customColumnWidths 995 self._ignoreSort = ignoreSort 996 self._showCollapseIcon = showCollapseIcon 997 self._columnOffset = 0 998 wnd = GetWindowAbove(self) 999 if wnd and not wnd.destroyed and hasattr(wnd, 'ShowLoad'): 1000 wnd.ShowLoad() 1001 self.sr.nodes = self.sr.entries = [] 1002 self.sr.content.Flush() 1003 self._position = self.sr.content.top = 0 1004 if showScrollTop: 1005 self._scrollbar_vertical.display = True 1006 if reversesort is None: 1007 reversesort = self.GetSortDirection() 1008 self.sortBy = sortby 1009 self.reversedSort = reversesort 1010 self.AddNodes(0, contentList, initing=True) 1011 if sortby and not ignoreSort: 1012 self.Sort(sortby, reversesort) 1013 if self.destroyed: 1014 return 1015 if noContentHint and not contentList: 1016 self.ShowHint(noContentHint) 1017 self.__LoadHeaders([]) 1018 else: 1019 self.ShowHint() 1020 self.__LoadHeaders(headers) 1021 if self.destroyed: 1022 return 1023 self.RefreshNodes(fromWhere='LoadContent') 1024 self.ScrollToProportion(scrollTo, threaded=False) 1025 self.UpdateTabStops('LoadContent') 1026 self.UpdateColumnLines() 1027 if wnd and not wnd.destroyed and hasattr(wnd, 'HideLoad'): 1028 wnd.HideLoad() 1029 self._loading = 0 1030 1031 Load = LoadContent 1032 1033 def LoadHeaders(self, headers): 1034 wnd = GetWindowAbove(self) 1035 try: 1036 if self.__LoadHeaders(headers): 1037 self.OnColumnChanged(self.sr.tabs) 1038 finally: 1039 if wnd and not wnd.destroyed and hasattr(wnd, 'HideLoad'): 1040 wnd.HideLoad() 1041 1042 @telemetry.ZONE_METHOD 1043 def __LoadHeaders(self, headers): 1044 self.sr.headers = headers 1045 self.UpdateTabStops('__LoadHeaders') 1046 if self.destroyed: 1047 return 1048 if headers: 1049 if not self.smartSort: 1050 sortby = self.GetSortBy() 1051 reversesort = self.GetSortDirection() 1052 if not sortby or sortby not in headers: 1053 sortby = headers[0] 1054 if not self._ignoreSort: 1055 self.Sort(sortby, reversesort) 1056 else: 1057 self.UpdatePositionThreaded(fromWhere='__LoadHeaders') 1058 else: 1059 self.Sort() 1060 if len(self.sr.nodes) or not headers: 1061 self.lastHeaders = headers 1062 return 1 1063 1064 def ResetColumnWidths(self): 1065 for header in self.GetColumns(): 1066 self.ResetColumnWidth(header) 1067 1068 def ResetColumnWidth(self, header, onlyReset = 0): 1069 if self.sr.id is None or self.refreshingColumns: 1070 return 1071 if not onlyReset: 1072 wnd = GetWindowAbove(self) 1073 if wnd and not wnd.destroyed and hasattr(wnd, 'ShowLoad'): 1074 wnd.ShowLoad() 1075 self.refreshingColumns = 1 1076 if header not in self.sr.fixedColumns and self.sr.id: 1077 headertab = [(header, 1078 self.headerFontSize, 1079 0, 1080 self.tabMargin, 1081 False)] 1082 else: 1083 headertab = [(header, 1084 self.headerFontSize, 1085 0, 1086 self.tabMargin, 1087 False)] 1088 if header in self.GetColumns(): 1089 idx = self.GetColumns().index(header) 1090 width = None 1091 if self._customColumnWidths: 1092 headerWidth = uicore.font.GetTextWidth(header, fontsize=self.headerFontSize, letterspace=0, uppercase=False) 1093 headerWidth += self.tabMargin * 2 1094 normHeader = header.replace('<br>', ' ') 1095 width = max([headerWidth] + [ node.GetColumnWidthFunction(None, node, normHeader) for node in self.sr.nodes if node.get('GetColumnWidthFunction', None) is not None ]) 1096 else: 1097 tabstops = self.GetTabStops(headertab, idx) 1098 if len(tabstops): 1099 width = max(MINCOLUMNWIDTH, tabstops[0]) 1100 if width is not None: 1101 current = settings.user.ui.Get('columnWidths_%s' % VERSION, {}) 1102 current.setdefault(self.sr.id, {})[header] = width 1103 settings.user.ui.Set('columnWidths_%s' % VERSION, current) 1104 self.UpdateTabStops('ResetColumnWidth') 1105 if not onlyReset and wnd and not wnd.destroyed and hasattr(wnd, 'HideLoad'): 1106 wnd.HideLoad() 1107 self.refreshingColumns = 0 1108 1109 @telemetry.ZONE_METHOD 1110 def ApplyTabstopsToNode(self, node, fromWhere = ''): 1111 if self.sr.ignoreTabTrimming or not self.GetColumns(): 1112 return 1113 tabStops = self.sr.tabs 1114 node.tabs = tabStops 1115 if tabStops and GetAttrs(node, 'panel', 'OnColumnResize'): 1116 cols = [] 1117 last = 0 1118 for tab in tabStops: 1119 cols.append(tab - last) 1120 last = tab 1121 1122 cols[0] -= self.sr.maincontainer.left 1123 node.panel.OnColumnResize(cols) 1124 elif tabStops and node.panel and node.panel.sr.label: 1125 label = node.panel.sr.label 1126 columnOffsetToUse = self.GetEffectiveColumnOffset() 1127 subTract = label.left - columnOffsetToUse 1128 if isinstance(label, LabelCore): 1129 newtext = self.GetStringFromNode(node) 1130 if newtext and (getattr(label, 'tabs', None) != tabStops or getattr(label, 'xShift', 0) != -subTract) and newtext.find('<t>') != -1: 1131 label.xShift = -subTract 1132 label.tabs = tabStops 1133 label.text = newtext 1134 label.Update() 1135 1136 def GetTabStops(self, headertabs, idx = None): 1137 strengir = [] 1138 fontsize = fontconst.DEFAULT_FONTSIZE 1139 letterspace = 0 1140 shift = 0 1141 self._columnOffset = 0 1142 for node in self.sr.nodes: 1143 if node.Get('GetSubContent', None): 1144 self._columnOffset = 16 1145 s = self.GetStrengir(node, fontsize, letterspace, shift, idx) 1146 if s is None: 1147 continue 1148 strengir.append(s) 1149 1150 return uicore.font.MeasureTabstops(strengir + headertabs, self.tabMargin) 1151 1152 def GetStrengir(self, node, fontsize, letterspace, shift, idx = None): 1153 t = self.GetStringFromNode(node) 1154 if not t or t.find('<t>') == -1: 1155 return 1156 if idx is not None: 1157 t = t.split('<t>') 1158 if len(t) <= idx: 1159 return 1160 t = t[idx] 1161 if node.panel and node.panel.sr.label: 1162 label = node.panel.sr.label 1163 fontsize = label.fontsize 1164 letterspace = label.letterspace 1165 shift = label.left 1166 return (t, 1167 fontsize, 1168 letterspace, 1169 shift, 1170 0) 1171 1172 @telemetry.ZONE_METHOD 1173 def UpdateTabStops(self, fromWhere = None, updatePosition = True): 1174 headers = self.GetColumns() 1175 headertabs = [] 1176 if headers is not None and len(headers): 1177 headertabs = [('<t>'.join(headers), 1178 self.headerFontSize, 1179 0, 1180 self.tabMargin, 1181 False)] 1182 tabstops = self.GetTabStops(headertabs) 1183 if self.sr.id and headers: 1184 userDefined = settings.user.ui.Get('columnWidths_%s' % VERSION, {}).get(self.sr.id, {}) 1185 i = 0 1186 columnOffsetToUse = self.GetEffectiveColumnOffset() 1187 total = columnOffsetToUse 1188 former = total 1189 for header in headers: 1190 if header in self.sr.fixedColumns: 1191 stopSize = self.sr.fixedColumns[header] 1192 else: 1193 userSetWidth = userDefined.get(header, None) or self.sr.defaultColumnWidth.get(header, None) 1194 minColumnWidth = self.sr.minColumnWidth.get(header, MINCOLUMNWIDTH) 1195 if userSetWidth is not None: 1196 stopSize = max(userSetWidth, minColumnWidth) 1197 else: 1198 stopSize = max(tabstops[i] - former, minColumnWidth) 1199 if header in self.sr.maxDefaultColumns: 1200 stopSize = min(self.sr.maxDefaultColumns.get(header, minColumnWidth), stopSize) 1201 total += stopSize 1202 former = tabstops[i] 1203 tabstops[i] = total 1204 i += 1 1205 1206 didChange = tabstops != self.sr.tabs 1207 self.sr.tabs = tabstops 1208 if self.sr.collapseCont and not self.sr.collapseCont.destroyed: 1209 collapseIconNeedsRefresh = False 1210 columnOffsetToUse = self.GetEffectiveColumnOffset() 1211 self.sr.collapseCont.display = bool(columnOffsetToUse) 1212 else: 1213 collapseIconNeedsRefresh = self._showCollapseIcon 1214 if headers != self.lastDrawnColumns or didChange or collapseIconNeedsRefresh: 1215 self.DrawHeaders(headers, tabstops) 1216 if didChange: 1217 if not self.smartSort: 1218 self.HiliteSorted(self.GetSortBy(), self.GetSortDirection(), 'UpdateTabStops') 1219 if not self._loading: 1220 self.OnColumnChanged(tabstops) 1221 if updatePosition: 1222 self.UpdatePositionThreaded('UpdateTabStops') 1223 return tabstops 1224 1225 @telemetry.ZONE_METHOD 1226 def AddNode(self, idx, node, isSub = 0, initing = False): 1227 if idx == -1: 1228 idx = len(self.sr.nodes) 1229 node.panel = None 1230 node.open = 0 1231 node.idx = idx 1232 node.isSub = isSub 1233 node.scroll = self.sr.selfProxy 1234 node.selected = node.get('isSelected', 0) 1235 if node.get('PreLoadFunction', None): 1236 node.PreLoadFunction(node) 1237 if self.destroyed: 1238 return 1239 self.sr.nodes.insert(idx, node) 1240 self.PrepareSubContent(node, initing=initing) 1241 return node 1242 1243 @telemetry.ZONE_METHOD 1244 def PrepareSubContent(self, node, initing = False, threadedUpdate = True): 1245 if node.id: 1246 if node.get('subNodes', []): 1247 rm = node.subNodes 1248 node.subNodes = [] 1249 node.open = 0 1250 self.RemoveNodes(rm) 1251 if node.Get('GetSubContent', None) is not None: 1252 forceOpen = node.get('forceOpen', False) and initing 1253 if forceOpen or uicore.registry.GetListGroupOpenState(node.id, default=node.get('openByDefault', False)): 1254 subcontent = node.GetSubContent(node) 1255 if self.destroyed or node not in self.GetNodes(): 1256 return 1257 if not node.Get('hideNoItem', False) and not len(subcontent): 1258 noItemText = node.get('noItemText', localization.GetByLabel('/Carbon/UI/Controls/Common/NoItem')) 1259 subcontent.append(self.GetNoItemNode(text=noItemText, sublevel=node.get('sublevel', 0) + 1)) 1260 if not self.destroyed: 1261 self.AddNodes(node.idx + 1, subcontent, node, initing=initing, threadedUpdate=threadedUpdate) 1262 if node not in self.GetNodes(): 1263 self.RemoveNodes(subcontent) 1264 else: 1265 node.subNodes = subcontent 1266 node.open = 1 1267 return subcontent 1268 1269 def SetNodes(self, nodes): 1270 self.sr.nodes = nodes 1271 self.RefreshNodes() 1272 1273 def _GetNodePanelName(self, node): 1274 if getattr(node, 'panelName', None): 1275 return node.panelName 1276 return node.name or 'entry_%s' % node.idx 1277 1278 @telemetry.ZONE_METHOD 1279 def RefreshNodes(self, fromWhere = None): 1280 if self.destroyed: 1281 return 1282 clipperWidth, clipperHeight = self.sr.clipper.GetCurrentAbsoluteSize() 1283 if not clipperWidth or not clipperHeight: 1284 clipperWidth, clipperHeight = self.sr.clipper.GetAbsoluteSize() 1285 _, inner_pad_top, _, inner_pad_bottom = self._innerPadding 1286 fromTop = inner_pad_top 1287 for nodeidx, node in enumerate(self.sr.nodes): 1288 node.idx = nodeidx 1289 nodeheight = self.GetNodeHeight(node, clipperWidth) 1290 node.scroll_positionFromTop = fromTop 1291 if node.panel: 1292 node.panel.align = uiconst.TOPLEFT 1293 node.panel.left = 0 1294 node.panel.top = fromTop 1295 node.panel.width = clipperWidth 1296 node.panel.height = nodeheight 1297 node.panel.name = self._GetNodePanelName(node) 1298 fromTop += nodeheight 1299 if self.shouldAddFinalRowPadding or nodeidx + 1 < len(self.sr.nodes): 1300 fromTop += self.rowPadding 1301 1302 for overlay, attrs, x, y in self.sr.overlays + self.sr.underlays: 1303 fromTop = max(fromTop, attrs.top + attrs.height) 1304 1305 atBottom = self._position and self._position == self.scrollingRange 1306 self._totalHeight = fromTop + inner_pad_bottom 1307 self.scrollingRange = max(0, self._totalHeight - clipperHeight) 1308 if self.scrollingRange and self.scrollEnabled: 1309 self._scrollbar_vertical.display = True 1310 else: 1311 self._scrollbar_vertical.display = False 1312 if self.scrollEnabled: 1313 if not self.scrollingRange or atBottom or self.stickToBottom: 1314 self._position = self.scrollingRange 1315 self._position = min(self._position, self.scrollingRange) 1316 else: 1317 self._position = 0 1318 self.UpdateContentSize(clipperWidth, clipperHeight, self._totalHeight) 1319 if self.sr.overlays_content: 1320 self.sr.overlays_content.height = self.sr.content.height 1321 self.sr.overlays_content.width = clipperWidth 1322 if self.sr.underlays_content: 1323 self.sr.underlays_content.height = self.sr.content.height 1324 self.sr.underlays_content.width = clipperWidth 1325 1326 @telemetry.ZONE_METHOD 1327 def UpdateContentSize(self, clipperWidth, clipperHeight, contentHeight): 1328 self.entriesTotalHeight = contentHeight 1329 self.on_content_resize(contentHeight) 1330 self.sr.content.height = max(clipperHeight, contentHeight) 1331 self.sr.content.width = clipperWidth 1332 1333 def GetEntriesTotalHeight(self): 1334 return self.entriesTotalHeight 1335 1336 @telemetry.ZONE_METHOD 1337 def AddNodes(self, fromIdx, nodesData, parentNode = None, ignoreSort = 0, initing = False, threadedUpdate = True): 1338 wnd = GetWindowAbove(self) 1339 if wnd and not wnd.destroyed and hasattr(wnd, 'ShowLoad'): 1340 wnd.ShowLoad() 1341 if fromIdx == -1: 1342 fromIdx = len(self.sr.nodes) 1343 isSub = 0 1344 if parentNode: 1345 isSub = parentNode.get('sublevel', 0) + 1 1346 nodes = [] 1347 idx = fromIdx 1348 for data in nodesData: 1349 newnode = self.AddNode(idx, data, isSub=isSub, initing=initing) 1350 if newnode is None: 1351 continue 1352 subs = self.CollectSubNodes(newnode, clear=0) 1353 idx = newnode.idx + 1 + len(subs) 1354 nodes.append(newnode) 1355 1356 if parentNode: 1357 parentNode.subNodes = nodes 1358 if not initing: 1359 if self.GetSortBy() and not (self._ignoreSort or ignoreSort): 1360 self.RefreshSort() 1361 elif threadedUpdate: 1362 self.RefreshNodes() 1363 self.UpdatePositionThreaded(fromWhere='AddNodes') 1364 else: 1365 self.RefreshNodes() 1366 self.UpdatePosition(fromWhere='AddNodes') 1367 if nodes: 1368 self.UpdateTabStops('AddNodes', updatePosition=True) 1369 if wnd and not wnd.destroyed and hasattr(wnd, 'HideLoad'): 1370 wnd.HideLoad() 1371 return nodes 1372 1373 AddEntries = AddNodes 1374 1375 @telemetry.ZONE_METHOD 1376 def RemoveNodes(self, nodes): 1377 wnd = GetWindowAbove(self) 1378 if wnd and not wnd.destroyed and hasattr(wnd, 'ShowLoad'): 1379 wnd.ShowLoad() 1380 subs = [] 1381 for node in nodes: 1382 subs.extend(self.CollectSubNodes(node)) 1383 1384 for nodeList in (nodes, subs): 1385 for node in nodeList: 1386 if node.panel: 1387 node.panel.Close() 1388 if node in self.sr.nodes: 1389 self.sr.nodes.remove(node) 1390 1391 self.RefreshNodes() 1392 self.UpdatePosition() 1393 if wnd and not wnd.destroyed and hasattr(wnd, 'HideLoad'): 1394 wnd.HideLoad() 1395 1396 RemoveEntries = RemoveNodes 1397 1398 @telemetry.ZONE_METHOD 1399 def CollectSubNodes(self, node, nodes = None, clear = 1): 1400 if nodes is None: 1401 nodes = [] 1402 inNodes = [ id(each) for each in nodes ] 1403 for subnode in node.get('subNodes', []): 1404 if subnode is None: 1405 continue 1406 self.CollectSubNodes(subnode, nodes, clear) 1407 if id(subnode) not in inNodes: 1408 nodes.append(subnode) 1409 1410 if clear: 1411 node.subNodes = [] 1412 return nodes 1413 1414 @telemetry.ZONE_METHOD 1415 def GetNodeHeight(self, node, clipperWidth): 1416 func = node.GetHeightFunction 1417 newStyle = getattr(node.decoClass, 'GetDynamicHeight', None) 1418 if func: 1419 allowDynamicResize = node.get('allowDynamicResize', True) 1420 if not node.height or allowDynamicResize and node._lastClipperWidth != clipperWidth: 1421 node.height = func(node.decoClass, node, clipperWidth) 1422 node._lastClipperWidth = clipperWidth 1423 elif newStyle: 1424 if not node.height or node._lastClipperWidth != clipperWidth: 1425 node.height = newStyle(node, clipperWidth) 1426 node._lastClipperWidth = clipperWidth 1427 elif self._fixedEntryHeight: 1428 node.height = self._fixedEntryHeight 1429 else: 1430 node.height = getattr(node.decoClass, 'ENTRYHEIGHT', 18) 1431 if not node.height: 1432 if func: 1433 func(None, node, clipperWidth) 1434 else: 1435 node.height = getattr(node.decoClass, 'ENTRYHEIGHT', 18) 1436 return node.height 1437 1438 @telemetry.ZONE_METHOD 1439 def GetContentWidth(self): 1440 w, h = self.GetContentParentSize() 1441 return w 1442 1443 @telemetry.ZONE_METHOD 1444 def GetContentHeight(self): 1445 return self._totalHeight 1446 1447 GetTotalHeight = GetContentHeight 1448 1449 def GetContentParentSize(self): 1450 w, h = self.sr.clipper.GetAbsoluteSize() 1451 return (w, h) 1452 1453 def UpdatePositionThreaded(self, fromWhere = None): 1454 if self.loopThread is None: 1455 self.loopThread = uthread.new(self.UpdatePositionLoop) 1456 self._pendingPositionUpdate = None 1457 else: 1458 self._pendingPositionUpdate = self._position 1459 1460 @telemetry.ZONE_METHOD 1461 def UpdatePositionLoop(self, fromWhere = None): 1462 if self.destroyed: 1463 return 1464 while True: 1465 nodeCreatedOrRefreshed, updatedPos = self.UpdatePosition(fromWhere='UpdatePositionLoop', doYield=True) 1466 if self._pendingPositionUpdate == updatedPos: 1467 self._pendingPositionUpdate = None 1468 if self.discardNonVisibleNodes: 1469 with ExceptionEater('Discarding non-visible nodes'): 1470 self.DiscardNonVisibleNodes() 1471 blue.pyos.BeNice() 1472 if self.destroyed: 1473 return 1474 if not nodeCreatedOrRefreshed: 1475 break 1476 1477 self.loopThread = None 1478 if self._pendingPositionUpdate is not None: 1479 uthread2.call_after_wallclocktime_delay(self._DelayedPositionCorrection, 0.5, self._pendingPositionUpdate) 1480 1481 def _DelayedPositionCorrection(self, pendingValue): 1482 if pendingValue == self._pendingPositionUpdate: 1483 self.UpdatePositionThreaded() 1484 1485 def DiscardNonVisibleNodes(self): 1486 for eachNode in self.GetNodes(): 1487 if eachNode.panel and not eachNode.panel.destroyed: 1488 if eachNode not in self.visibleNodes: 1489 eachNode.panel.Close() 1490 eachNode.panel = None 1491 1492 @telemetry.ZONE_METHOD 1493 def UpdatePosition(self, fromWhere = None, doYield = False): 1494 if self.destroyed: 1495 return (None, None) 1496 clipperWidth, clipperHeight = self.sr.clipper.GetAbsoluteSize() 1497 self.sr.content.top = int(-self._position) 1498 if self.sr.overlays_content: 1499 self.sr.overlays_content.top = self.sr.content.top 1500 if self.sr.underlays_content: 1501 self.sr.underlays_content.top = self.sr.content.top 1502 self.UpdateScrollHandle(clipperHeight, fromWhere='UpdatePosition') 1503 tabStops = self.sr.tabs 1504 scrollPosition = self._position 1505 ignoreTabstops = self.sr.ignoreTabTrimming or not self.GetColumns() 1506 nodeLoaded = False 1507 self.visibleNodes = [] 1508 for nodeCount, node in enumerate(self.sr.nodes): 1509 if nodeCount != node.idx or node.scroll_positionFromTop is None or node.height is None: 1510 self.RefreshNodes(fromWhere='UpdatePosition') 1511 nodeheight = node.height 1512 posFromTop = node.scroll_positionFromTop 1513 displayScrollEntry = node.panel 1514 if scrollPosition > posFromTop + nodeheight or scrollPosition + clipperHeight < posFromTop: 1515 if displayScrollEntry: 1516 displayScrollEntry.state = uiconst.UI_HIDDEN 1517 if doYield: 1518 blue.pyos.BeNice() 1519 continue 1520 forceTabstops = False 1521 if not displayScrollEntry or displayScrollEntry.destroyed: 1522 decoClass = node.decoClass 1523 displayScrollEntry = decoClass(parent=self.sr.content, align=uiconst.TOPLEFT, pos=(0, 1524 posFromTop, 1525 clipperWidth, 1526 nodeheight), state=uiconst.UI_NORMAL, name=node.name or 'entry_%s' % node.idx, node=node) 1527 displayScrollEntry.sr.node = node 1528 node.panel = displayScrollEntry 1529 node.scroll = self.sr.selfProxy 1530 if hasattr(displayScrollEntry, 'Startup'): 1531 displayScrollEntry.Startup(self.sr.selfProxy) 1532 displayScrollEntry.Load(node) 1533 forceTabstops = True 1534 nodeLoaded = True 1535 elif not displayScrollEntry.display: 1536 displayScrollEntry.state = uiconst.UI_NORMAL 1537 if not node.isStatic: 1538 displayScrollEntry.Load(node) 1539 forceTabstops = True 1540 nodeLoaded = True 1541 elif getattr(displayScrollEntry, 'currentBandingValue', None) is not None: 1542 if node.idx % getattr(displayScrollEntry, 'MOD_DIV', 2) != displayScrollEntry.currentBandingValue and getattr(displayScrollEntry, 'UpdateBandingColor', None): 1543 displayScrollEntry.UpdateBandingColor() 1544 displayScrollEntry.UpdateBandingFillVisibility() 1545 self.visibleNodes.append(node) 1546 if not ignoreTabstops: 1547 updateTabs = node.tabs != tabStops 1548 if forceTabstops or updateTabs: 1549 self.ApplyTabstopsToNode(node, 'UpdatePosition') 1550 1551 self.OnUpdatePosition(self) 1552 return (nodeLoaded, scrollPosition) 1553 1554 def UpdateScrollHandle(self, clipperHeight, fromWhere = ''): 1555 if self.destroyed or not self._scrollbar_vertical: 1556 return 1557 if not self._totalHeight: 1558 handle_size_fraction = 0.0 1559 else: 1560 handle_size_fraction = clipperHeight / float(self._totalHeight) 1561 self._scrollbar_vertical.handle_size_fraction = mathext.clamp(handle_size_fraction, 0.0, 1.0) 1562 if not self._suppress_scroll_vertical_update: 1563 scroll_fraction = 0.0 1564 if self._position and self.scrollingRange: 1565 scroll_fraction = self._position / float(self.scrollingRange) 1566 self._scrollbar_vertical.scroll_to_fraction(scroll_fraction) 1567 1568 def OnChar(self, enteredChar, *args): 1569 if enteredChar < 32: 1570 return False 1571 if not self.sr.nodes: 1572 return False 1573 haveCharIndex = False 1574 for node in self.sr.nodes: 1575 if node.charIndex is not None: 1576 haveCharIndex = True 1577 break 1578 1579 if not haveCharIndex: 1580 return False 1581 if blue.os.TimeAsDouble(blue.os.GetWallclockTime()) - self.lastCharReceivedAt < 1.0 and self.currChars is not None: 1582 self.currChars += unichr(enteredChar).lower() 1583 else: 1584 self.currChars = unichr(enteredChar).lower() 1585 if enteredChar == uiconst.VK_SPACE: 1586 selected = self.GetSelected() 1587 if len(selected) == 1 and self.currChars == ' ' and GetAttrs(selected[0], 'panel', 'OnCharSpace') is not None: 1588 selected[0].panel.OnCharSpace(enteredChar) 1589 return True 1590 uthread.new(self._OnCharThread, enteredChar) 1591 self.lastCharReceivedAt = blue.os.TimeAsDouble(blue.os.GetWallclockTime()) 1592 return True 1593 1594 def ScrollToSelectedNode(self): 1595 selected = self.GetSelected() 1596 if selected: 1597 self.ScrollToNode(selected[0]) 1598 1599 def ScrollToNode(self, node): 1600 numEntries = len(self.sr.nodes) 1601 if numEntries <= 1: 1602 return 1603 entryPos = self.sr.nodes.index(node) 1604 self.ScrollToProportion(float(entryPos) / (numEntries - 1)) 1605 1606 def _OnCharThread(self, enteredChar): 1607 if self.destroyed: 1608 return 1609 charsBefore = self.currChars 1610 blue.pyos.synchro.SleepWallclock(100) 1611 if self.destroyed: 1612 return 1613 if self.currChars != charsBefore: 1614 return 1615 selected = self.GetSelected() 1616 if not selected: 1617 selected = self.sr.nodes 1618 selected = selected[0] 1619 numEntries = len(self.sr.nodes) 1620 if selected not in self.sr.nodes: 1621 return 1622 startIndex = self.sr.nodes.index(selected) 1623 if len(self.currChars) == 1: 1624 startIndex += 1 1625 entryRange = range(numEntries)[startIndex:] + range(numEntries)[:startIndex] 1626 for i in entryRange: 1627 entry = self.sr.nodes[i] 1628 if entry.charIndex and entry.charIndex.lower().startswith(self.currChars): 1629 self.SelectNode(entry) 1630 self.ScrollToNode(entry) 1631 break 1632 1633 def OnDelete(self): 1634 pass 1635 1636 def OnUpdatePosition(self, *args): 1637 pass 1638 1639 def ShowLoading(self): 1640 if not self.loadingWheel: 1641 self.loadingWheel = LoadingWheel(name='myLoadingWheel', parent=self, align=uiconst.CENTER, width=32, height=32) 1642 self.ShowHint(None) 1643 1644 def HideLoading(self): 1645 if self.loadingWheel: 1646 self.loadingWheel.Close() 1647 self.loadingWheel = None 1648 1649 def CheckOverlaysAndUnderlays(self): 1650 for overlay, attrs, x, y in self.sr.overlays + self.sr.underlays: 1651 if overlay is None or overlay.destroyed: 1652 continue 1653 if attrs.Get('align', None) == 'right': 1654 overlay.left = self.GetContentWidth() - overlay.width - attrs.left 1655 if not overlay.loaded: 1656 overlay.top = attrs.top 1657 overlay.SetAlign(uiconst.RELATIVE) 1658 overlay.state = uiconst.UI_NORMAL 1659 overlay.Load() 1660 1661 def GetNode(self, idx, checkInternalNodes = False): 1662 if checkInternalNodes: 1663 allNodes = self.GetNodes(allowNone=True) 1664 else: 1665 allNodes = self.sr.nodes 1666 if idx == -1: 1667 if allNodes: 1668 return allNodes[-1] 1669 else: 1670 return None 1671 if len(allNodes) > idx: 1672 return allNodes[idx] 1673 1674 def OnKeyDown(self, key, flag): 1675 if uiconst.VK_DELETE == key: 1676 self.OnDelete() 1677 elif key == uiconst.VK_PRIOR: 1678 self.ScrollByPage(up=True) 1679 elif key == uiconst.VK_NEXT: 1680 self.ScrollByPage(up=False) 1681 1682 def ScrollByPage(self, up = True): 1683 visibleNodes = self.visibleNodes 1684 numVisibleNodes = len(visibleNodes) 1685 allNodesNum = len(self.sr.nodes) 1686 if numVisibleNodes < 1: 1687 return 1688 if up: 1689 lastVisibleNode = visibleNodes[0] 1690 step = -1 1691 else: 1692 lastVisibleNode = visibleNodes[-1] 1693 step = 1 1694 newNodesHeight = lastVisibleNode.height 1695 currentNodeIdx = lastVisibleNode.idx 1696 clipperWidth, clipperHeight = self.GetContentParentSize() 1697 while 1: 1698 nextNodeIdx = currentNodeIdx + step 1699 if not 0 < nextNodeIdx < allNodesNum - 1: 1700 break 1701 nextNode = self.GetNode(idx=nextNodeIdx, checkInternalNodes=False) 1702 newNodesHeight += nextNode.height 1703 if newNodesHeight > clipperHeight: 1704 break 1705 currentNodeIdx = nextNodeIdx 1706 1707 self.ShowNodeIdx(currentNodeIdx, threadedScrolling=False) 1708 1709 def Resizing(self): 1710 pass 1711 1712 def OnClipperResize(self, clipperWidth, clipperHeight, *args, **kw): 1713 self.OnContentResize(clipperWidth, clipperHeight, *args, **kw) 1714 1715 def OnContentResize(self, clipperWidth, clipperHeight, *args, **kw): 1716 if self.sr.hint: 1717 w, h = clipperWidth, clipperHeight 1718 newWidth = w - self.sr.hint.left * 2 1719 if abs(newWidth - self.sr.hint.width) > 12: 1720 self.sr.hint.width = newWidth 1721 if self.sr.content: 1722 self.RefreshNodes(fromWhere='OnContentResize') 1723 self.UpdatePositionThreaded(fromWhere='OnContentResize') 1724 if not self.destroyed: 1725 self.Resizing() 1726 1727 def BrowseNodes(self, up): 1728 sel = self.GetSelected() 1729 control = uicore.uilib.Key(uiconst.VK_CONTROL) 1730 shift = uicore.uilib.Key(uiconst.VK_SHIFT) 1731 if sel: 1732 shiftIdx = None 1733 if not control and shift: 1734 r = [ node.idx for node in sel ] 1735 if up: 1736 if r[0] < self.lastSelected: 1737 shiftIdx = r[0] - 1 1738 else: 1739 shiftIdx = r[-1] - 1 1740 elif r[0] < self.lastSelected: 1741 shiftIdx = r[0] + 1 1742 else: 1743 shiftIdx = r[-1] + 1 1744 if shiftIdx is None: 1745 if len(sel) > 1: 1746 idx = sel[[-1, 0][up]].idx 1747 else: 1748 idx = sel[-1].idx 1749 idx += [1, -1][up] 1750 else: 1751 idx = shiftIdx 1752 total = len(self.GetNodes()) 1753 if 0 <= idx < total: 1754 self.ActivateIdx(idx) 1755 return 1 1756 return 0 1757 1758 BrowseEntries = BrowseNodes 1759 1760 def OnUp(self): 1761 if not self.GetSelected(): 1762 self.ActivateIdx(len(self.sr.nodes) - 1) 1763 return 1764 if not self.BrowseNodes(1): 1765 self.Scroll(1 + 10 * uicore.uilib.Key(uiconst.VK_SHIFT)) 1766 1767 def OnDown(self): 1768 if not self.GetSelected(): 1769 self.ActivateIdx(0) 1770 return 1771 if not self.BrowseNodes(0): 1772 self.Scroll(-1 - 10 * uicore.uilib.Key(uiconst.VK_SHIFT)) 1773 1774 def OnHome(self): 1775 self.ScrollToProportion(0.0) 1776 1777 def OnEnd(self): 1778 self.ScrollToProportion(1.0) 1779 1780 def OnMouseWheel(self, *etc): 1781 if getattr(self, 'wheeling', 0): 1782 return 1783 if HasContextMenu(): 1784 menuOwner = GetContextMenuOwner() 1785 if menuOwner and menuOwner.IsUnder(self): 1786 return 1787 self.wheeling = 1 1788 self.Scroll(uicore.uilib.dz / 240.0) 1789 self.wheeling = 0 1790 1791 def Scroll(self, dz): 1792 if not self.scrollEnabled: 1793 return 1794 step = 100 1795 pos = max(0, min(self.scrollingRange, self._position - step * dz)) 1796 if pos != self._position: 1797 self._position = int(pos) 1798 self.stickToBottom = False 1799 self.UpdatePositionThreaded(fromWhere='Scroll') 1800 1801 def ScrollPixels(self, delta): 1802 if not self.scrollEnabled: 1803 return 1804 pos = mathext.clamp(self._position - delta, 0, self.scrollingRange) 1805 if pos != self._position: 1806 self._position = int(pos) 1807 self.stickToBottom = False 1808 self.UpdatePositionThreaded(fromWhere='ScrollPixels') 1809 1810 def GetScrollProportion(self): 1811 if self.scrollingRange: 1812 return self._position / float(self.scrollingRange) 1813 return 0.0 1814 1815 @telemetry.ZONE_METHOD 1816 def ScrollToProportion(self, proportion, threaded = True): 1817 if not self.scrollEnabled: 1818 return 1819 proportion = min(1.0, max(0.0, proportion)) 1820 pos = int(max(0, self.scrollingRange * proportion)) 1821 self._position = int(pos) 1822 if threaded: 1823 self.UpdatePositionThreaded(fromWhere='ScrollToPorportion') 1824 else: 1825 self.UpdatePosition(fromWhere='ScrollToPorportion') 1826 1827 def GetMinSize(self): 1828 return (64, 64) 1829 1830 def GetMaxTextWidth(self, defaultTextWidth = 0): 1831 nodes = self.GetNodes() 1832 if not nodes: 1833 return defaultTextWidth 1834 textWidths = [] 1835 for node in nodes: 1836 fontsize = node.Get('fontsize', DEFAULT_FONTSIZE) 1837 hspace = node.Get('letterspace', DEFAULT_LETTERSPACE) 1838 uppercase = node.Get('uppercase', DEFAULT_UPPERCASE) 1839 textWidth = uicore.font.GetTextWidth(node.label, fontsize, hspace, uppercase) 1840 padLeft = node.Get('padLeft', self.tabMargin) 1841 padRight = node.Get('padRight', self.tabMargin) 1842 padIndentation = uiconst.ENTRY_DEFAULT_ICONSIZE * (1 + node.get('sublevel', 0)) 1843 textWidth += padIndentation + padLeft + padRight 1844 textWidths.append(textWidth) 1845 1846 return max(textWidths) 1847 1848 def GetNoItemNode(self, text, sublevel = 0, *args): 1849 return GetFromClass(Generic, {'label': text, 1850 'sublevel': sublevel}) 1851 1852 def Copy(self, *args): 1853 myNodes = self.GetSelected() or self.GetNodes() 1854 allLabelsList = [] 1855 for node in myNodes: 1856 decoClass = node.decoClass 1857 if decoClass is None: 1858 report_node_missing_deco_class(node) 1859 continue 1860 GetCopyData = getattr(decoClass, 'GetCopyData', None) 1861 if GetCopyData is None: 1862 continue 1863 label = GetCopyData(node) 1864 if label: 1865 allLabelsList.append(label) 1866 1867 allLabels = '\n'.join(allLabelsList) 1868 allLabels = allLabels.replace('<t>', '\t') 1869 strippedText = StripTags(allLabels) 1870 if strippedText: 1871 blue.pyos.SetClipboardData(strippedText) 1872 numEntriesCopied = len(allLabelsList) 1873 text = localization.GetByLabel('UI/Common/EntriesCopied', numEntries=numEntriesCopied) 1874 message_player(text) 1875 1876 def AddCollapseBtn(self): 1877 self.sr.collapseCont = Container(parent=self.sr.scrollHeaders, name='collapseCont', pos=(4, 0, 12, 0), align=uiconst.TOLEFT, idx=0 if self.isLinelessScroll else 1) 1878 ButtonIcon(name='collapse', parent=self.sr.collapseCont, align=uiconst.CENTERLEFT, pos=(0, 0, 12, 12), iconSize=12, texturePath='res:/UI/Texture/classes/Scroll/Collapse.png', func=self.CollapseAll, hint=localization.GetByLabel('UI/Common/Buttons/CollapseAll')) 1879 1880 def CollapseAll(self, *args): 1881 for eachNode in self.GetNodes(): 1882 if eachNode.Get('GetSubContent', None): 1883 uicore.registry.SetListGroupOpenState(eachNode.id, 0) 1884 1885 self.ReloadNodes() 1886 1887 def GetEffectiveColumnOffset(self): 1888 if not self._showCollapseIcon: 1889 return 0 1890 return self._columnOffset 1891 1892 def Startup(self, minZ = None): 1893 pass 1894 1895 def SetDefaultSmartSorting(self, column, sortOrder, availableColumns = None): 1896 if not self.sr.id: 1897 return 1898 try: 1899 self._SetDefaultSmartSortingPrimary(column, availableColumns) 1900 self._SetDefaultSmartSortingDirection(column, sortOrder) 1901 except Exception as e: 1902 log.exception('Failed to set deault smart sorting') 1903 1904 def _SetDefaultSmartSortingPrimary(self, column, availableColumns): 1905 primaryColumns = settings.user.ui.Get(PRIMARY_COLUMN % VERSION, {}) 1906 currentPrimaryForScroll = primaryColumns.get(self.sr.id, None) 1907 if currentPrimaryForScroll: 1908 if availableColumns and currentPrimaryForScroll not in availableColumns: 1909 primaryColumns[self.sr.id] = column 1910 else: 1911 primaryColumns[self.sr.id] = column 1912 settings.user.ui.Set(PRIMARY_COLUMN % VERSION, primaryColumns) 1913 1914 def _SetDefaultSmartSortingDirection(self, column, sortOrder): 1915 sortDirections = settings.user.ui.Get(SMART_SORT_DIRECTION % VERSION, {}) 1916 directionsForScroll = sortDirections.get(self.sr.id, None) 1917 if directionsForScroll is None: 1918 directionsForScroll = sortDirections[self.sr.id] = {} 1919 if column not in directionsForScroll: 1920 directionsForScroll[column] = sortOrder 1921 settings.user.ui.Set(SMART_SORT_DIRECTION % VERSION, sortDirections) 1922 1923 1924 def report_node_missing_deco_class(node): 1925 scroll_path = '' 1926 scroll = getattr(node, 'scroll', None) 1927 if scroll is not None: 1928 scroll_path = gather_element_path(scroll) 1929 scroll_path = ' at {}'.format(scroll_path) 1930 log.error('Found a %s node without a decoClass property%s', getattr(node, '__guid__', None), scroll_path) 1931 1932 1933 def gather_element_path(element): 1934 path = None 1935 while element: 1936 name = getattr(element, 'name', None) 1937 if name is None: 1938 element_type = getattr(element, '__class__', None) 1939 if element_type is not None: 1940 name = '[class:{}]'.format(element_type.__name__) 1941 else: 1942 name = '[unknown]' 1943 path = '{}.{}'.format(name, path) if path is not None else name 1944 element = getattr(element, 'parent', None) 1945 1946 return path