#pragma rtGlobals=1 // Use modern global access method. // ================================================================================================= // Stickies. Just type away while your mouse is over a graph to create a text box. // W. Harneit, March 2011 // ================================================================================================= // // Usage: 1. Create a graph, bring it to the front. // 2. Call InstallStickies() for the top graph window or choose "use stickies in top graph" from the Stickies menu. // 3. Point the mouse somewhere inside the graph, start typing. That's it. // // - To "freeze" the stickies and stop keyboard interpretation, call InstallStickies(stop=1) . // - To remove all sticky notes, call InstallStickies(wipe=1) . // - Commentize (=> Edit menu) or delete the next lines to suppress the menu for these actions. // // ================================================================================================= menu "Stickies" "start using stickies in top graph", /Q, InstallStickies() "stop using stickies in top graph", /Q, InstallStickies(stop=1) "wipe stickies from top graph", /Q, InstallStickies(wipe=1) "\\M0stop + wipe stickies (top graph)", /Q, InstallStickies(wipe=1, stop=1) end // ================================================================================================= // // Details: - A new "sticky" textbox is created whenever you type at a new position in the graph (after activation, of course) // - If the mouse is over an existing sticky, that sticky will be changed instead. // - The new input is then inserted at the position you last were at, which is remembered for each sticky. // - To see the cursor position before typing, click on the sticky // - The cursor will be removed when you move the mouse a little // - To start a new sticky in front of an existing one, press the "escape" button on your keyboard. // - To kill an existing sticky, hit "shift-escape" while the mouse is over the sticky. // - The normal navigation keys (left,up,right,down,home,end,page up,page down) work as expected, // but marking a part of the text by holding down shift key is not possible at the moment. // - Pressing shift + navigation keys will move the sticky instead (slow for "shift+left", fast for "shift+home", etc.) // // Technical: Implemented as a named window hook function. // Blinking cursor implemented as a background function. // Uses custom structure and window userdata for each sticky (no global variables!) // // ================================================================================================= // ================================================================================================= // these constants can be modified at will to customize your stickies. // ================================================================================================= constant kStickyColor = 0xFFFF80 // default background color (light yellow) in html rgb format (one byte for r,g,b each) constant kStickyNudge = 1 // movement of sticky box in percent of window size in response to (shift + cursor keys) constant kStickyMove = 10 // movement of sticky box in percent of window size in response to (shift + cursor keys) constant kStickyFrame = 2 // frame code, 0=none, 1=underline, 2=box strconstant ksStickyBaseName = "sticky" // if you put "base" here, the real names will be base0, base1, ... // ================================================================================================= // ================================================================================================= // custom structure to store information about a sticky box // ================================================================================================= structure StickyInfoStruct char name[32] // unique identifier (based on ksStickyBase) struct Point pos // position relative to graph window int16 csr // cursor, where data will be added (not visible) endstructure // ================================================================================================= function InstallStickies([winName, wipe, stop]) string winName variable wipe, stop winName = SelectString( ParamIsDefault(winName) || (strlen(winName) == 0), winName, StringFromList(0,WinList("*",";","WIN:1")) ) DoWindow/F/HIDE=0 $winName if( V_Flag ) if( !ParamIsDefault(wipe) && wipe ) WipeStickies(winName) endif if( ParamIsDefault(stop) || !stop ) SetWindow $winName, hook(stickyHook) = StickyWinHook else SetWindow $winName, hook(stickyHook) = $"" endif endif end function WipeStickies(winName) string winName variable rVal = 1 // make sure window is graph if( WhichListItem(winName, WinList("*", ";", "WIN:1")) < 0 ) return 0 // nothing done endif // make a list of stickies in this window string stickies = ListMatch(AnnotationList(winName), ksStickyBaseName+"*") // remove stickies one by one variable k for( k = 0; k < ItemsInList(stickies); k += 1 ) rVal *= KillSticky(winName, StringFromList(k, stickies)) endfor SetWindow $winName, userdata(lastSticky)="" return rVal // zero if at least one kill failed end function KillSticky(winName, stickyName) string winName, stickyName if( WhichListItem(winName, WinList("*", ";", "WIN:1")) < 0 ) return 0 // nothing done endif if( WhichListItem(stickyName, AnnotationList(winName)) >= 0 ) // fail-safe if winName valid SetWindow $winName, userdata(laststicky)="" TextBox /W=$winName /K /N=$stickyName SetWindow $winName, userdata($stickyName)="" SetWindow $winName, userdata($(stickyName+"text"))="" endif return 1 end function StickyWinHook(s) struct WMWinHookStruct &s variable rVal = 0 struct StickyInfoStruct sticky string text, UDStr switch( s.eventCode ) case 4: // "mouse moved", deactivate last sticky UDStr =GetUserData(s.winName, "", "lastSticky") if( strlen(UDStr) ) // we have an active last sticky // stop BlinkCursor background function CtrlNamedBackground BlinkCursorTask, stop // remove caret character StructGet /S sticky, UDStr text = GetUserData(s.winName, "", sticky.name+"text") text[sticky.csr,sticky.csr] = "" TextBox /C /N=$sticky.name text SetWindow $s.winName, UserData($(sticky.name+"text")) = text SetWindow $s.winName, UserData(lastSticky) = "" endif break case 5: // "mouse up", activate sticky if( FindSticky(s, sticky) ) // retrieves sticky structure in case of success UDStr =GetUserData(s.winName, "", "lastSticky") if( !strlen(UDStr) ) // we have no active last sticky, and hence must insert the cursor text = GetUserData(s.winName, "", sticky.name+"text") text[sticky.csr] = "|" TextBox /C /N=$sticky.name text SetWindow $s.winName, UserData($(sticky.name+"text")) = text StructPut /S sticky, UDStr SetWindow $s.winName, UserData(lastSticky) = UDStr endif CtrlNamedBackground BlinkCursorTask, proc=BlinkCursor, period=30, start endif break case 11: // "keyboard", catch typing // prevent creation of invisible sticky outside of window // (can happen if window not deactivated but mouse far outside) if( s.mouseLoc.h < s.winRect.left || s.mouseLoc.h > s.winRect.right ) break elseif( s.mouseLoc.v < s.winRect.top || s.mouseLoc.v > s.winRect.bottom ) break endif // find or create sticky if( !FindSticky(s, sticky) ) // sets up sticky structure in case of success NewSticky(s, sticky) // sets up sticky structure from scratch endif // all the hard work is done in: DoStickyKeyEvent(s, sticky) // will modify and store sticky structure, and start BlinkCursorTask break endswitch return rVal end // returns boolean value, sets name and position in case of success function FindSticky(whs, sticky) struct WMWinHookStruct &whs struct StickyInfoStruct &sticky variable locH = whs.mouseLoc.h variable locV = whs.mouseLoc.v // first try the easy way: if mouse hasn't moved since the last action, we pass back the last edited sticky string UDStr = GetUserData(whs.winName, "", "lastSticky") if( strlen(UDStr) ) // lastSticky only exists after first sticky has been written StructGet /S sticky, UDStr if( locH == sticky.pos.h && locV == sticky.pos.v ) StructGet /S sticky, GetUserData(whs.winName, "", sticky.name) return 1 endif endif // okay, the hard way: look if mouse happens to be within any sticky rectangle, respecting front-to-back ordering string annoList = AnnotationList(whs.winName) variable p2p = ScreenResolution / 72 // converts pixels to points variable k, top, left, bottom, right string infoStr, rectStr, stickyName for( k = ItemsInList(annoList); k > 0; k -= 1 ) // top-most come last infoStr = AnnotationInfo(whs.winName, StringFromList(k-1, annoList)) rectStr =StringByKey("RECT", infoStr) // in points left = p2p * str2num(StringFromList(0, rectStr, ",")) top = p2p * str2num(StringFromList(1, rectStr, ",")) right = p2p * str2num(StringFromList(2, rectStr, ",")) bottom = p2p * str2num(StringFromList(3, rectStr, ",")) if( left < locH && locH < right && top < locV && locV < bottom ) stickyName = StringByKey("N", StringByKey("FLAGS", infoStr), "=", "/") StructGet /S sticky, GetUserData(whs.winName, "", stickyName) // update position to reflect current TextBox, which may have been moved by user sticky.pos.h = left sticky.pos.v = top return 1 endif endfor // not found if we come by here return 0 end function NewSticky(whs, sticky) struct WMWinHookStruct &whs struct StickyInfoStruct &sticky sticky.name = UniqueName(ksStickyBaseName, 14, 0, whs.winName) // type = 14 : Annotation sticky.csr = 0 sticky.pos.h = whs.mouseLoc.h-1 // to be sure that the box is in focus, sticky.pos.v = whs.mouseLoc.v-1 // we put it slightly end function StoreSticky(whs, sticky, text) struct WMWinHookStruct &whs struct StickyInfoStruct &sticky string text // store the sticky... string UDStr StructPut /S sticky, UDStr SetWindow $whs.winName, userdata($sticky.name) = UDStr // ... and its text SetWindow $whs.winName, userdata($(sticky.name+"text")) = text // store as last sticky, with present mouse location sticky.pos.h = whs.mouseLoc.h sticky.pos.v = whs.mouseLoc.v StructPut /S sticky, UDStr SetWindow $whs.winName, userdata(lastSticky) = UDStr end function DoStickyKeyEvent(whs, sticky) struct WMWinHookStruct &whs struct StickyInfoStruct &sticky variable handledKey = 1 // we handle ALL keys variable key = whs.keycode variable mods = whs.eventMod string win = whs.winName string text = GetUserData(win, "", sticky.name+"text") // *** text[sticky.csr,sticky.csr] = "" // *** variable /C LineChar = TextCursor2LineOffset(text, sticky.csr) switch( key ) case 8: // backspace, remove precious character if( sticky.csr > 0) sticky.csr -= 1 text[sticky.csr,sticky.csr] = "" endif break case 127: // del, remove next character if( sticky.csr <= strlen(text)) text[sticky.csr,sticky.csr] = "" endif break case 28: // left if( mods & 2 ) // shift: nudge sticky left sticky.pos.h -= kStickyNudge // note: not limiting deliberately else // no shift: move cursor left sticky.csr = max(sticky.csr-1, 0) endif break case 29: // right if( mods & 2 ) // shift: nudge sticky right sticky.pos.h += kStickyNudge // note: not limiting deliberately else // no shift: move cursor right sticky.csr = min(sticky.csr+1, strlen(text)) endif break case 30: // up if( mods & 2 ) // shift: nudge sticky up sticky.pos.v -= kStickyNudge // note: not limiting deliberately else // no shift: move cursor up one line sticky.csr = LineOffset2TextCursor(text, real(LineChar) - 1, imag(LineChar)) endif break case 31: // down if( mods & 2 ) // shift: nudge sticky down sticky.pos.v += kStickyNudge // note: not limiting deliberately else // no shift: move cursor down one line sticky.csr = LineOffset2TextCursor(text, real(LineChar) + 1, imag(LineChar)) endif break case 1: // home if( mods & 2 ) // shift: move sticky left sticky.pos.h -= kStickyMove // note: not limiting deliberately else // no shift: move cursor to line (paragraph) start sticky.csr = LineOffset2TextCursor(text, real(LineChar), 0) endif break case 4: // end if( mods & 2 ) // shift: move sticky right sticky.pos.h += kStickyMove // note: not limiting deliberately else // no shift: move cursor to line (paragraph) end sticky.csr = LineOffset2TextCursor(text, real(LineChar), inf) endif break case 11: // page up if( mods & 2 ) // shift: move sticky up sticky.pos.v -= kStickyMove // note: not limiting deliberately else // no shift: move cursor up to first line sticky.csr = LineOffset2TextCursor(text, 0, imag(LineChar)) endif break case 12: // page down if( mods & 2 ) // shift: move sticky down sticky.pos.v += kStickyMove // note: not limiting deliberately else // no shift: move cursor down to last line sticky.csr = LineOffset2TextCursor(text, inf, imag(LineChar)) endif break case 27: // escape if( mods & 2 ) // shift: kill text box if( !KillSticky(win, sticky.name) ) print "BUG (DoStickyKeyEvent): sticky may have survived kill attempt" endif return handledKey // we're done already else // no shift: start new text box NewSticky(whs, sticky) text = "" endif break case 92: // backslash, may have to be doubled text[sticky.csr+1,sticky.csr] = num2char(key) sticky.csr += 1 // fall through! case 9: // tabulator, no special action case 13: // carriage return, no special action default: // key > 31 && key != 127, normal printable character text[sticky.csr] = num2char(key) // this syntax INSERTS the character sticky.csr += 1 endswitch // *** text[sticky.csr] = "|" CtrlNamedBackground BlinkCursorTask, proc=BlinkCursor, period=30, start // *** variable tbX = 100 * sticky.pos.h / (whs.winRect.right - whs.winRect.left) variable tbY = 100 * sticky.pos.v / (whs.winRect.bottom - whs.winRect.top) variable r = (kStickyColor & 0xff0000) / 0x100 variable g = kStickyColor & 0xff00, b = (kStickyColor & 0xff) * 0x100 TextBox /W=$win /C /A=LT /B=(r,g,b) /E=2 /F=(kStickyFrame)/N=$sticky.name /X=(tbX) /Y=(tbY) text StoreSticky(whs, sticky, text) return handledKey end // Blinking cursor as a named background function // started after mouseup (StickyWinHook) or keyboard event over existing sticky (StickyWinHook via DoStickyKeyEvent) // stopped after mousemoved event (StickyWinHook) function BlinkCursor(s) STRUCT WMBackgroundStruct &s string text variable code struct StickyInfoStruct sticky string UDStr = GetUserData("", "", "lastSticky") // from top window if( strlen(UDStr) ) StructGet /S sticky, UDStr text = GetUserData("", "", sticky.name+"text") code = char2num(text[sticky.csr]) if( code == 124) text[sticky.csr,sticky.csr] = " " elseif( code == 32) text[sticky.csr,sticky.csr] = "|" endif TextBox /C /N=$sticky.name text SetWindow kwTopWin, userdata($(sticky.name+"text")) = text endif return 0 end // function/C TextCursor2LineOffset(str, tcsr) // - returns the position of textcursor TCSR in string STR in terms of // - a line number (returned as the real part) and // - an offset relative to the start of that line (imaginary part). // - TCSR is an index pointing to a position _between_ two characters in string STR // - TCSR is forced to the limits 0 and strlen(STR) (max. tcsr is one past string end) // - LINE runs from 0 (before first ) to numLines (after last ) // - OFFSET runs from 0 (before first character in line) to numCharsInLine (after last char.) function/C TextCursor2LineOffset(str, tcsr) string str variable tcsr tcsr = limit(tcsr, 0, strlen(str)) // enforce limits variable line = 0 // no counted yet variable pos = strsearch(str, "\r", tcsr-1, 1) // position of last in str (before tcsr) variable offset = tcsr-1 - max(0, pos) // relative offset, max is for "no " case do if( pos <= 0 ) // no more in str (backward search)? if( pos == 0 ) // ...yes, but is first character line += (tcsr != 0) // ...if so, we have one more line (if not tcsr==0) endif break // ...done in both cases. endif line += 1 // otherwise, increment line count pos = strsearch(str, "\r", pos-1, 1) // look for next (backward search) while( 1 ) return cmplx(line, offset+(line==0)) // if line==0, compensate for missing end // function LineOffset2TextCursor(str, line, offset) // - returns position of text cursor in string STR equivalent to LINE/OFFSET specification // - is the inverse function to TextCursor2LineOffset, see above // - out-of-range line/offset values are clipped to legal values, i.e, they are treated thus: // - if LINE is out of range (< 0 or > numLines), it is clipped to legal values // - if OFFSET is out of range (< 0 or > numCharsInLine), // it is clipped to legal values pertaining to that line // - this means you get the same answer for (line=-1 and line=0), for (offset=-1 and offset=0), // for (line=inf and line=numLines) and for (offset=inf and offset=numCharsInLine), etc. function LineOffset2TextCursor(str, line, offset) string str variable line, offset variable len = strlen(str) variable pos, oldpos, start = 0 do pos = strsearch(str, "\r", start) // look for next (forward search) oldpos = start // remember last before this one if( pos < 0 ) // no more ? pos = len // adjust clipping range for offset break // done. endif start = pos+1 // prepare to look for next line -= 1 // count down... while( line >= 0 ) // ... to zero (eliminates one loop variable) return oldpos + limit(offset, 0, pos-oldpos) // position of last before last found + offset limited to legal range end