5
$\begingroup$

I have an InputField that searches a predefined list for matching strings and displays them in a TextGrid. But when the list is large, the front end hangs:

enter image description here

Using Cases is obviously slow, but I don't want to use the fourth argument to limit the number of matches, so can't see any simple way to speed it up:

Clear @ SearchStrings
SearchStrings[input_:""] := DynamicModule[{$input=input, matches = {}},
	Panel @ Column @ {
		Row[{InputField[Dynamic[$input], String,
			FieldHint->"Enter company", FieldHintStyle->{Gray},
			BaseStyle->{"ControlStyle"},ContinuousAction->True,
			FieldSize->13], Dynamic @ Text @ Style[" (" <> ToString @ Length[matches]<> " rows)", 11, FontFamily->"Verdana",
				FontWeight->"Thin", FontColor->Gray]}, "   "],
		Pane[
			Dynamic[
				TextGrid[
					Map[{Button[Text @ Style[#, 13, FontFamily->"Verdana",
								FontWeight->"Thin", FontColor->Black], 
								CopyToClipboard[#], Appearance->"Frameless"]}&,
						matches = Cases[$sites, s_/;StringContainsQ[s,$input], {-1}]
					],
					ItemSize->{40,Automatic}, Dividers->All, Spacings->2, FrameStyle -> Directive[Thickness[Tiny], LightGray]
				, Selectable -> False],
			TrackedSymbols :> {$input}],
        Scrollbars->{False,True}, ImageSize->{230,200}]}
]

$sites = ToString /@ Range[10000];

SearchStrings[""]
$\endgroup$
  • $\begingroup$ Side note, there is FieldCompletionFunction option for InputField now. $\endgroup$ – Kuba Jun 5 '18 at 18:01
  • $\begingroup$ related autocompletion topics: 102470, 28529 related grid interface topic: 21117 $\endgroup$ – Kuba Jun 5 '18 at 18:13
  • 1
    $\begingroup$ p.s. Cases is not slow. Typesetting and rendering of the long list of matched entries is. That is the problem in the 21117 question too. $\endgroup$ – Kuba Jun 5 '18 at 18:32
  • $\begingroup$ Thanks, I did know about FieldCompletionFunction - but wanted to implement more permanent view of the results. Do you think something like UITableView that supports streaming data into a scrolling view is possible to write ourselves? $\endgroup$ – M.R. Jun 5 '18 at 20:44
  • $\begingroup$ Yes, but at a cost of items size flexibility and using custom scrollbars which will not render as nicely as OS versions. Plus it needs to be created too. $\endgroup$ – Kuba Jun 6 '18 at 7:55
4
$\begingroup$

The bottleneck is the fact that Pane 'cares' about full content even when only the fraction is needed. We need Pane that fits our needs better.

The solution is based on a beta version of VerticalScrollbar and ListPane, if you find it hard to customize, let me know about options it should take, syntax etc.

It was only tested on Win7.

Let me know what changes you;d like to see in a polished version.

enter image description here

VerticalScrollbar // ClearAll ;

VerticalScrollbar // Options = {
  "PageSize" -> .2
, ImageSize -> Automatic  
};

VerticalScrollbar[Dynamic[scrollPosition_], OptionsPattern[] ]:=With[
  { pageSize = OptionValue["PageSize"]
  , MPY     := MousePosition[{"GraphicsScaled", Graphics}, {0,0}][[2]]
  , thumbCol = GrayLevel @ .9
  , thumbColActive = GrayLevel @ .8
  , scrollHeight = .2
  , imageSize  = OptionValue[ImageSize] /. Automatic -> {15, Full}

  }
, DynamicModule[
    { 
      showScrollbar,            
      scrollRawPosition = 1 - (scrollPosition + scrollHeight),
      mousePositionStart,
      scrollRawPositionOffset = 0,
      clipSRPos = Function[pos, Clip[pos, {0 , 1 - scrollHeight }]],
      col = GrayLevel[.9]
    }
  , 
   Module[
      {pane, scrollbar}

    , scrollbar = Graphics[
        DynamicNamespace @ { EdgeForm@Black, Dynamic@If[CurrentValue["MouseOver"], thumbColActive, thumbCol]
        , Rectangle[{0, Dynamic[scrollRawPosition]}, {1, Dynamic[scrollRawPosition + scrollHeight]}, BoxID -> "thumb"]      
        , Black, Inset["=", DynamicLocation["thumb", None, Center]]
        }      
      , ImageSize        -> imageSize
      , PlotRange        -> {{0,1}, {0,1}}
      , PlotRangePadding -> {2. / 15, 2./200}
      , ImageMargins->0
      , PlotRangeClipping->False
      , AspectRatio->Full


      , ImagePadding->None
      , Background->GrayLevel@.95
      ]

    ; scrollbar = EventHandler[
        scrollbar
      , {
          "MouseDown" :> (
            mousePositionStart = MPY
          ; Which[            (*inside, maybe better vie MouseEntered?*)
              mousePositionStart < scrollRawPosition
            , scrollRawPosition = clipSRPos[scrollRawPosition - pageSize]

            , mousePositionStart > scrollRawPosition + scrollHeight  
            , scrollRawPosition = clipSRPos[scrollRawPosition + pageSize]

            , True, scrollRawPositionOffset = MPY - scrollRawPosition 
            ]
          )
        , "MouseDragged" :> (
            scrollRawPosition = clipSRPos[MPY - scrollRawPositionOffset]
          )

        }  
      ]  
        (* visibility *)
    ; scrollbar = PaneSelector[
        { True -> scrollbar, False-> Spacer[0]}
      , Dynamic[showScrollbar]
      ]
        (* listener*)
    ; scrollbar = DynamicWrapper[scrollbar    
      , scrollPosition = 1 - Rescale[scrollRawPosition, {0 , 1-scrollHeight}, {0, 1}]
      , TrackedSymbols :> {scrollRawPosition}
      ]


    ; scrollbar  
    ]
  ]
];

ListPane // ClearAll;

ListPane // Options = {
   ImageSize -> {All, 200},
   "ItemWrapper" -> (Pane[##, ImageMargins->0]&)
};

ListPane[Dynamic[list_], OptionsPattern[]]:=With[
  {
    paneHeight = OptionValue[ImageSize][[2]],
    itemHeight = 25,
    totalLength = Length[list],
    imageSize = OptionValue[ImageSize],
    itemWrapper = OptionValue["ItemWrapper"]
  }
, DynamicModule[
    {
      visibleLength = Ceiling[paneHeight/itemHeight],
      visiblePart,
      position = 1,
      scrollPosition = 0

    }
  , visibleLength = Ceiling[paneHeight/itemHeight]
  ; visiblePart = Take[list,UpTo[visibleLength]]


  ; Module[
      {pane}

    , pane = Dynamic[ (*TODO: preburn*)
        Column[
          itemWrapper[#, ImageSize->{Full, itemHeight},ImageMargins->0, Alignment->{Left,Center}]&/@visiblePart
        , Spacings->0
        ]
      ]
    ; Panel[
        Grid[{{
          pane
        , DynamicWrapper[
            VerticalScrollbar[Dynamic@scrollPosition, ImageSize -> {15, paneHeight}]
          , position = Round @ Rescale[scrollPosition, {0,1}, {1, totalLength}]
          ; visiblePart = Take[list, {position, UpTo[position + visibleLength]}]
          , TrackedSymbols:>{scrollPosition}
          ]

        }}, Spacings->{1,0},Alignment->{Left,Top}]
      , ImageSize->{imageSize[[1]], paneHeight+visibleLength-1}
      , FrameMargins->0
      , Alignment->{Right,Top}
      , BaseStyle->CacheGraphics->False
      ] 
    ]
  ]
];

And your updated code

Clear@SearchStrings
SearchStrings[input_: ""] := 
 DynamicModule[{$input = input, matches = {}},
  Panel[
   Column@{Row[{
       InputField[Dynamic[$input], String, 
        FieldHint -> "Enter company", FieldHintStyle -> {Gray}, 
        BaseStyle -> {"ControlStyle"}, ContinuousAction -> True, 
        FieldSize -> 13],
       Dynamic@
        Text@Style[" (" <> ToString@Length[matches] <> " rows)", 11, 
          FontFamily -> "Verdana", FontWeight -> "Thin", 
          FontColor -> Gray]
       }, "   "
      ],
     DynamicWrapper[
      Dynamic[
       ListPane[
        Dynamic[matches], ImageSize -> {Full, 200}, 
        "ItemWrapper" -> (Button[
            Text@Style[#, 13, FontFamily -> "Verdana", 
              FontWeight -> "Thin", FontColor -> Black], 
            CopyToClipboard[#], Appearance -> "Frameless", ##2] &)
        ],
       TrackedSymbols :> {matches}
       ],
      matches = Cases[$sites, s_ /; StringContainsQ[s, $input], {-1}],
      TrackedSymbols :> {$input}
      ]

     }
   ,
   ImageSize -> {300, All}]
  ]

$sites = ToString /@ Range[10000];

Deploy@SearchStrings[""]
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.