At the top of every DrRacket editor window is a toolbar with buttons:
As with syntax coloring and indenting, these buttons can be customized for any Racket-implemented language.
For jsonic, a useful button might be one that inserts our expression delimiters @$ ··· $@ and automatically moves the cursor between them.
So let’s add that.
When DrRacket notices that jsonic is being used in a source file, it will call our get-info function with a drracket:toolbar-buttons query. We need to return a list of buttons that DrRacket will install in the toolbar.
We already did some of the setup. At the end of indenting, we left get-info with its drracket:toolbar-buttons branch commented out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax get-info) (define (get-info port src-mod src-line src-col src-pos) (define (handle-query key default) (case key [(color-lexer) (dynamic-require 'jsonic/colorer 'color-jsonic)] [(drracket:indentation) (dynamic-require 'jsonic/indenter 'indent-jsonic)] #;[(drracket:toolbar-buttons) (dynamic-require 'jsonic/buttons 'button-list)] [else default])) handle-query))
Below, we’ll write a module that will provide button-list. After that, we’ll go back and uncomment the corresponding branch of get-info so everything works together.
As "jsonic/main.rkt" indicates, our list of buttons will be called button-list and live in a module called "jsonic/buttons.rkt". So let’s start that file now:
Our button-list is, of course, a list. Each button within this list is itself a list of four values:
A string representing the button label shown in the toolbar.
A 16-pixel-high bitmap representing the icon that goes next to the label.
A function that’s called when the button is pressed. This function receives a single argument: a reference to the editor window where the button was pressed.
A number that determines the ordering of the button on the toolbar. If we don’t care about this, we can set it to #f.
We’re only going to have one button called our-jsonic-button, but we could have more.
Let’s use Insert expression as the button label, and #f as our ordering number. That takes care of two out of four fields:
For our bitmap, in honor of the iconic Kazimir Malevich, we’ll use a white square. We can quickly draw this square by importing racket/draw and calling make-object bitmap%:
1 2 3 4 5 6 7 8 9 10 11 12
But the most important part of the button is the button-press function. We’ll define a new function called button-func and add it as the third value in our button:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#lang br (require racket/draw) (define (button-func drr-window) (define expr-string "@$ $@") (define editor (send drr-window get-definitions-text)) (send editor insert expr-string) (define pos (send editor get-start-position)) (send editor set-position (- pos 3))) (define our-jsonic-button (list "Insert expression" (make-object bitmap% 16 16) button-func #f)) (provide button-list) (define button-list (list our-jsonic-button))
Let’s walk through the lines of this function.
Our single argument drr-window is a reference to the current DrRacket window. (We can skip writing a contract for this function because it’ll only be used by DrRacket, which can be trusted to pass the correct kind of argument.)
The drr-window object is an instance of the frame% class in Racket’s GUI library (similar to how the text box in the indenter was an instance of the text% class). Because DrRacket passes every button a reference to the whole window, a button can do pretty much anything to the window: change the text, draw on the screen, and so on. But that also means that writing button functions requires some familiarity with the GUI library.
To avoid that detour, we’ll use a little extra handwaving as we walk through the code above, which inserts our expression delimiters separated by two spaces @$ $@, and then moves the insertion point into the middle.
To use methods stored in Racket objects, we send an object a method name and any necessary arguments. To start, we grab a reference to the upper pane of the window containing the source with get-definitions-text, and store it in editor. We then insert our expr-string in the editor. By default, it will go wherever the insertion point is positioned, just the same as if we typed it.
After the insertion, the insertion point will end up at the end of the expr-string. But we want it to be in the middle of the inserted text. So first, we get-start-position to find out where the insertion point is. Then we subtract 3 (because our expr-string is 6 characters long) and pass this adjusted position to set-position, which will move the insertion point to the right place.
It’s not a miracle of software engineering. But it will work.
To use our button in DrRacket, we have to connect it to our get-info function in "main.rkt". We do that by first uncommenting the branch we previously commented out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#lang br/quicklang (module reader br (require "reader.rkt") (provide read-syntax get-info) (define (get-info port src-mod src-line src-col src-pos) (define (handle-query key default) (case key [(color-lexer) (dynamic-require 'jsonic/colorer 'color-jsonic)] [(drracket:indentation) (dynamic-require 'jsonic/indenter 'indent-jsonic)] [(drracket:toolbar-buttons) (dynamic-require 'jsonic/buttons 'button-list)] [else default])) handle-query))
Then, as we did in syntax coloring and indenting, we force a refresh of get-info. If we’re using Racket v6.9 or later, we select Racket → Reload #lang Extensions, which reloads our get-info function and our new colorer. If not, we quit and restart DrRacket, which has the same effect.
Once we do, our new button will show up on the left edge of the toolbar:
Then if we select some code in the window:
And click the Insert expression button:
The selected code will be replaced with our expression delimiters, and the insertion point moved within:
As with syntax coloring and indenting, DrRacket toolbar buttons are completely optional. They don’t affect anything about how our language works. We can ignore them entirely. But they’re a nice way of integrating our language with DrRacket.