diff --git a/info.rkt b/info.rkt new file mode 100644 index 0000000..2d55599 --- /dev/null +++ b/info.rkt @@ -0,0 +1,25 @@ +#lang info + +(define pkg-authors '(hnmdijkema)) +(define version "0.1.1") +(define license 'MIT) +(define collection "racket-sprintf") +(define pkg-desc "racket-sprintf - simple sprintf implementation") + +(define scribblings + '( + ("scribblings/sprintf.scrbl" () () "sprintf") + ) + ) + +(define deps + '("racket/base" + ) + ) + +(define build-deps + '("racket-doc" + "scribble-lib" + ) + ) + diff --git a/scribblings/sprintf.scrbl b/scribblings/sprintf.scrbl new file mode 100644 index 0000000..290efa3 --- /dev/null +++ b/scribblings/sprintf.scrbl @@ -0,0 +1,655 @@ +#lang scribble/manual + +@(require scribble/manual + ;scribble/class + (for-label racket/base + racket/class + racket/gui/base + net/sendurl + json + ;gregor + ;gregor/time + net/sendurl + racket/path + "../private/web-wire.rkt" + "../private/webui-wire-download.rkt" + "../private/webui-wire-ipc.rkt" + "../private/css.rkt" + "../private/menu.rkt" + "../private/web-racket.rkt")) + +@title{High-Level Web Racket GUI API} +@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] + +@defmodule[web-racket] + +The @racket[web-racket] module provides a higher-level GUI layer on top +of the low-level @racketmodname[web-wire] backend. It is intended as the +main entry point for desktop applications that embed a WebUI. + +The module exports: + +@itemlist[ + @item{OO wrappers for DOM elements and form inputs + (@racket[ww-element%], @racket[ww-input%]);} + @item{classes for main windows and dialogs + (@racket[ww-webview%], @racket[ww-webview-dialog%], + @racket[ww-webview-message%]);} + @item{an abstract settings interface (@racket[ww-settings%]);} + @item{backend control and logging helpers re-exported from + @racketmodname[web-wire];} + @item{backend installation/override helpers re-exported from + @racketmodname[webui-wire-download];} + @item{CSS and menu construction helpers re-exported from + @racket["css.rkt"] and @racket["menu.rkt"].} +] + +The goal of this layer is: + +@itemlist[ + @item{You rarely call @racket[web-wire] directly;} + @item{you work with windows, elements, and inputs as Racket objects;} + @item{only when you need fine-grained control do you drop to the + underlying wire API.} +] + +@section{Re-exported backend control and logging} + +The following bindings are re-exported directly from +@racketmodname[web-wire]. See that module’s documentation for full +details; here is a short summary for convenience. + +@defproc[(ww-start [log-level symbol?]) web-rkt?]{ + +Start the Web Wire backend if it is not running already, and return the +current backend handle. + +Most applications do not call this directly: @racket[ww-webview%] +constructors will automatically call @racket[ww-start] when the first +window is created. +} + +@defproc[(ww-stop) void?]{ + +Stop the Web Wire backend. + +This is called automatically when the last window created via +@racket[ww-webview%] is closed; you normally do not need to call it +directly, but you can use it for explicit shutdown. +} + +@defproc[(ww-set-debug [on? boolean?]) void?]{ + +Enable or disable internal debug logging in the @racket[web-wire] +layer. This affects @racket[ww-debug] output. +} + +@defform*[[(ww-debug msg-expr) #:contracts ([msg-expr any/c]) + (ww-debug id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c]) + ]]{ +Debug logging macros that write to the shared log buffer when debug is +enabled. +} + +@defform*[[(ww-error msg-expr) #:contracts ([msg-expr any/c]) + (ww-error id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c]) + ]]{ +Error logging macros that always log, regardless of the debug setting. +} + +@defproc[(ww-display-log + [filter (or/c #f string? regexp? (listof string?)) #f]) + void?]{ + +Display the in-memory log buffer on @racket[current-output-port], +optionally filtered. +} + +@defproc[(ww-tail-log [args any/c] ...) void?]{ + +Show the tail of the log buffer and follow new entries (like +@tt{tail -f}). Optional arguments control how many lines to show +initially and which lines to filter. +} + +@section{Re-exported WebUI helper command control} + +The following bindings are re-exported from +@racketmodname[webui-wire-download]; they are documented in more detail +in that module. Briefly: + +@defproc[(ww-set-custom-webui-wire-command! + [cmd string?]) + string?]{ + +Override the command used to start the @tt{webui-wire} helper. Useful in +development or when the helper is installed in a non-standard location. +} + +@defproc[(ww-get-webui-wire-version) string?]{ + +Return the version string reported by the installed @tt{webui-wire} +helper. +} + +@section{Re-exported CSS and menu helpers} + +The module also re-exports: + +@itemlist[ + @item{@racket[(all-from-out "css.rkt")] — CSS helper functions and + @racket[css-style] values;} + @item{@racket[(all-from-out "menu.rkt")] — menu construction helpers + for building menu structures passed to @racket[ww-webview%].} +] + +See the documentation of those modules for the exact API. + +@section{Element wrapper classes} + +These classes provide OO wrappers around DOM elements managed by +@tt{webui-wire}. They work on top of the lower-level @racket[web-wire] +functions that address elements by id. + +@subsection{Generic element} + +@defclass[ww-element% object% ()]{ + +Base class for element wrappers. + +@defconstructor/auto-super[([win-id ww-win?] + [id (or/c symbol? string?)])] + +The constructor normally isn’t called directly by user code. Objects are +created automatically by @racket[ww-webview%] when you bind inputs and +buttons or when you explicitly request an element. + +Fields: + +@itemlist[ + @item{@racket[win-id] — a @racket[ww-win] handle identifying the WebUI + window that owns this element} + @item{@racket[id] — the element id (symbol or string), as used in the + DOM.} +] + +Public methods: + +@defmethod[(get-win-id) ww-win?]{ +Return the window handle used for this element.} + +@defmethod[(get-id) (or/c symbol? string?)]{ +Return the element id.} + +@defmethod[(win) (or/c ww-webview% #f)]{ +Look up the @racket[ww-webview%] instance for @racket[win-id] in the +global window table. Returns @racket[#f] if the window no longer +exists.} + +@defmethod[(callback [evt symbol?] [args any/c] ...) any]{ + +Call a previously registered callback for @racket[evt] with the +given @racket[args]. This is used internally when DOM events +arrive from @tt{webui-wire}.} + +@defmethod[(connect [evt symbol?] [func (-> any)]) void?]{ + +Register @racket[func] as the handler for event kind @racket[evt] on +this element.} + +@defmethod[(disconnect [evt symbol?]) void?]{ + +Remove any callback registered for @racket[evt].} + +@defmethod[(add-style! [st css-style?]) void?]{ +Merge the given style into the element’s existing style.} + +@defmethod[(set-style! [st css-style?]) void?]{ +Replace the element’s style with @racket[st].} + +@defmethod[(style) css-style?]{ +Return the current style as a @racket[css-style] value.} + +@defmethod[(get-attr [a (or/c symbol? string?)]) jsexpr?]{ +Get a single attribute value as JSON.} + +@defmethod[(set-attr! [a (or/c symbol? string?)] [val any/c]) void?]{ +Set an attribute value.} + +@defmethod[(del-attr [a (or/c symbol? string?)]) void?]{ +Remove an attribute.} + +@defmethod[(get-attrs) (hash/c symbol? any/c)]{ +Return all attributes as a hash table (symbol → value).} + +@defmethod[(add-class! [cl (or/c symbol? string?)]) void?]{ +Add a CSS class to the element.} + +@defmethod[(remove-class! [cl (or/c symbol? string?)]) void?]{ +Remove a CSS class.} + +@defmethod[(has-class? [cl (or/c symbol? string?)]) boolean?]{ +Test whether the element has a given CSS class.} + +@defmethod[(enable) void?]{ +Remove the @tt{disabled} state from the element +(by manipulating its CSS/attributes).} + +@defmethod[(enabled?) boolean?]{ +Return @racket[#t] when the element is not disabled.} + +@defmethod[(disable) void?]{ +Mark the element as disabled (e.g. by adding a @tt{disabled} attribute +and/or class).} + +@defmethod[(disabled?) boolean?]{ +Return @racket[#t] when the element is disabled.} + +@defmethod[(display [d string? "block"]) void?]{ +Set the CSS @tt{display} style; default is @tt{"block"}.} + +@defmethod[(hide) void?]{ +Convenience for @racket[(display "none")].} + +@defmethod[(show) void?]{ +Show the element again (typically restoring display to a non-@tt{"none"} +value).} + +} + +@subsection{Form input element} + +@defclass[ww-input% ww-element% ()]{ + +Wrapper for input-type elements (fields that have a “value” and that +emit change/input events). Instances are usually created by +@racket[ww-webview%] when you call @racket[bind-inputs]. + +Internally, a @racket[ww-input%] keeps track of: + +@itemlist[ + @item{the current value (mirrored from the DOM);} + @item{an optional change callback.} +] + +Public methods: + +@defmethod[(get) any/c]{ +Return the last known value of the input.} + +@defmethod[(on-change! [callback (-> any/c any)]) void?]{ + +Register a change callback: + +@itemlist[ + @item{The callback is stored;} + @item{It is immediately called once with the current value;} + @item{Later, when an @tt{input} event with a @tt{value} field arrives + from the browser, the stored callback is called with the new + value.} +] +} + +@defmethod[(set! [v any/c]) void?]{ + +Set the input’s value both locally and in the DOM. This will normally +update the form control on screen and may also trigger change/input +events in the browser. +} + +@defmethod[(disable) void?]{Disable the input (overrides base behaviour).} + +@defmethod[(enable) void?]{Enable the input again (overrides base +behaviour).} +} + +@section{Webview windows} + +@subsection{Main windows} + +@defclass[ww-webview% object% ()]{ + +Represents a WebUI window driven by the @tt{webui-wire} backend. + +@defconstructor/auto-super[ + ([profile symbol? 'default-profile] + [settings (or/c #f ww-settings%) #f] + [use-browser boolean? #f] + [parent-id (or/c #f ww-win?) #f] + [parent (or/c #f ww-webview%) #f] + [title string? "Racket HTML Window"] + [x number?] + [y number?] + [width number?] + [height number?] + [icon (or/c #f path? string?) #f] + [menu any/c #f] + [html-file (or/c #f path? string?) #f]) +]{ + +The numeric geometry defaults (@racket[x], @racket[y], @racket[width], +@racket[height]) are taken either from @racket[settings] or from builtin +defaults. + +If @racket[parent] or @racket[parent-id] is provided, a child window +is created and positioned relative to the parent; otherwise a normal +top-level window is created. + +If @racket[menu], @racket[icon], or @racket[html-file] are provided, +they are applied after window creation. +} + +Important behaviour during construction: + +@itemlist[ + @item{when the first window is created, @racket[ww-start] is called;} + @item{the new window is registered in the global @racket[windows] + table;} + @item{an event handler is installed so that incoming events from + @tt{webui-wire} are dispatched to methods like + @racket[handle-click] and @racket[handle-change];} + @item{when the last window is destroyed, @racket[ww-stop] is called.} +] + +Key public methods (grouped by responsibility): + +@bold{Settings and cloning} + +@defmethod[(clone-settings [section symbol?]) (or/c #f ww-settings%)]{ + +If @racket[settings] is non-@racket[#f], call @racket[clone] on it +to obtain a section-specific settings object, otherwise return +@racket[#f]. +} + +@bold{Event handling hooks} + +These are normally invoked internally when events arrive from +@tt{webui-wire}, but can be overridden in subclasses. + +@defmethod[(handle-click [element-id symbol?] [data jsexpr?]) void?]{ +Find the element object for @racket[element-id] (if present) and call +its @racket[callback] method with the @racket['click] event.} + +@defmethod[(handle-change [element-id symbol?] [data jsexpr?]) void?]{ +Find the element object and delegate a @racket['change] event.} + +@defmethod[(handle-input [element-id symbol?] [data jsexpr?]) void?]{ +Find the element object and delegate an @tt{input} event.} + +@defmethod[(handle-navigate [url string?] + [type symbol?] + [kind symbol?]) + void?]{ + +Handle navigation events (for example link clicks) as reported by +@tt{webui-wire}. The default implementation may call +@racket[send-url] depending on @racket[type] and @racket[kind].} + +@bold{Element management} + +@defmethod[(get-win-id) ww-win?]{ +Return the underlying @racket[ww-win] handle.} + +@defmethod[(element [id (or/c symbol? string?)]) ww-element%]{ + +Return (and lazily create) an element wrapper for a given DOM id. +The concrete class is chosen based on the element’s tag and type +(@racket[ww-input%] for input fields, or @racket[ww-element%] +otherwise). +} + +@defmethod[(get-elements [selector selector?]) + (listof ww-element%)]{ + +Query elements matching @racket[selector] via @racket[ww-get-elements] +and return a list of element wrapper objects. +} + +@defmethod[(bind [event symbol?] + [selector selector?] + [forced-cl (or/c #f (is-a?/c ww-element%)) #f]) + void?]{ + +Bind @racket[event] to all elements matching @racket[selector]. + +A suitable element class is chosen automatically based on tag/type, +unless @racket[forced-cl] is provided; in that case the given class +is used for all matched elements. +} + +@defmethod[(bind-inputs) boolean?]{ +Convenience: bind @racket['change] events for @tt{input} and +@tt{textarea} elements and create @racket[ww-input%] wrappers.} + +@defmethod[(bind-buttons) void?]{ +Convenience: bind @racket['click] events for @tt{button} elements.} + +@bold{Window geometry and title} + +@defmethod[(move [x number?] [y number?]) void?]{ +Move the window to (@racket[x], @racket[y]).} + +@defmethod[(resize [x number?] [y number?]) void?]{ +Resize the window to width @racket[x], height @racket[y].} + +@defmethod[(get-x) number?]{Current x position (cached).} +@defmethod[(get-y) number?]{Current y position (cached).} +@defmethod[(get-width) number?]{Current width (cached).} +@defmethod[(get-height) number?]{Current height (cached).} + +@defmethod[(geom) (list number? number? number? number?)]{ +Return @racket[(list x y width height)].} + +@defmethod[(set-title! [t string?]) void?]{ +Set the window title (cached locally and sent to the backend).} + +@defmethod[(get-title) string?]{ +Return the last title set.} + +@bold{Show/hide and lifetime} + +@defmethod[(show) void?]{ +Show the window (set show state to @racket['show]).} + +@defmethod[(hide) void?]{ +Hide the window.} + +@defmethod[(maximize) void?]{ +Maximise the window.} + +@defmethod[(normalize) void?]{ +Restore the window to its normal state.} + +@defmethod[(minimize) void?]{ +Minimise the window.} + +@defmethod[(fullscreen) void?]{ +Request a fullscreen display (if supported).} + +@defmethod[(show-state) symbol?]{ +Return the current show state as reported by the backend.} + +@defmethod[(can-close?) boolean?]{ +Return @racket[#t] if the window may be closed; subclasses can +override this to veto close requests.} + +@defmethod[(close) void?]{ + +Close the window, unregister it from the global tables, and if this is +the last window, stop the backend by calling @racket[ww-stop]. +} + +@bold{Menus} + +@defmethod[(set-menu! [menu-def is-menu?]) void?]{ +Set the window menu using a structure created with the helpers from +@racket["menu.rkt"].} + +@defmethod[(connect-menu! [id symbol?] [cb (-> any)]) void?]{ + +Associate a callback @racket[cb] with a menu item id. When the menu +item is activated, the callback is called. +} + +@defmethod[(popup-menu [menu-def is-menu?]) void?]{ +Pops up a contextmenu at the current cursor position. +The menu is created using a structure created with the helpers from +@racket["menu.rkt"].} + + +@bold{HTML and navigation} + +@defmethod[(set-icon! [icn (or/c path? string?)]) void?]{ +Set the window icon; the file is validated by @racket[ww-set-icon].} + +@defmethod[(set-html-file! [file (or/c path? string?)]) void?]{ + +Load the given HTML file. The directory part is used to adjust +@racket[ww-cwd], and the file name is passed to +@racket[ww-set-html-file].} + +@defmethod[(set-html [html string?]) void?]{ + +Write @racket[html] to a temporary file and call +@racket[set-html-file!] with it. Intended for simple dynamic content.} + +@defmethod[(set-url [url string?]) void?]{ +Open the given URL using @racket[send-url].} + +@defmethod[(html-loaded) void?]{ + +Hook that is called when a page is fully loaded and matches the +current HTML handle. The default implementation binds buttons and +inputs by calling @racket[bind-buttons] and @racket[bind-inputs]. +} + +@bold{File and directory dialogs} + +@defmethod[(file-open [caption string?] + [base-dir string?] + [filters string?]) + (or/c #f path?)]{ + +Show a file-open dialog and return the selected path as a @racket[path], +or @racket[#f] if the operation failed or was cancelled. +} + +@defmethod[(file-save [caption string?] + [base-dir string?] + [filters string?] + [overwrite boolean? #f]) + (or/c #f path?)]{ + +Show a file-save dialog. The @racket[overwrite] flag controls whether +overwriting existing files is allowed. Returns the chosen path, or +@racket[#f] if cancelled or if the command failed. +} + +@defmethod[(choose-dir [caption string?] + [base-dir string?]) + (or/c #f path?)]{ + +Show a directory chooser dialog; return the chosen directory, or +@racket[#f] on cancel/failure. +} + +@bold{Hook for subclasses} + +@defmethod[(inherit-checks) boolean?]{ + +Called early during construction to allow subclasses to enforce +preconditions (for example: “dialog windows must have a parent”). +The default implementation simply returns @racket[#t]. +} +} + + + + +@subsection{Dialog windows} + +@defclass[ww-webview-dialog% ww-webview% ()]{ + +Subclass of @racket[ww-webview%] representing dialog-style windows. + +The constructor is the same as @racket[ww-webview%], but +@racket[inherit-checks] is overridden to enforce that a @racket[parent] +was supplied: + +@racketblock[ + (define/override (inherit-checks) + (when (eq? parent #f) + (error "A parent must be given"))) +] + +So, to create a dialog, always pass a parent window: + +@racketblock[ + (define main (new ww-webview%)) + (define dlg (new ww-webview-dialog% [parent main])) +] +} + + + +@subsection{Message dialogs} + +@defclass[ww-webview-message% ww-webview-dialog% ()]{ + +Simple message-dialog subclass. + +The constructor: + +@itemlist[ + @item{calls the @racket[ww-webview-dialog%] constructor;} + @item{sets a minimal HTML page containing a message header and a + submessage, with known element ids (e.g. @tt{"msg"} and + @tt{"submsg"}).} +] + +You can then obtain element wrappers for those ids via +@racket[send] @racket[this] @racket[element] and update their inner +HTML or text using the usual element methods. +} + + + +@section{Settings abstraction} + +@defclass[ww-settings% object% ()]{ + +Abstract base class for storing and retrieving configuration values +used by @racket[ww-webview%] (for example window geometry). + +The default implementation only signals errors; you are expected to +subclass @racket[ww-settings%] and override the methods. + +Public methods: + +@defmethod[(set [key symbol?] [value any/c]) void?]{ +Set a configuration value for @racket[key]. Default implementation +raises an error.} + +@defmethod[(get [key symbol?] [default any/c]) any/c]{ + +Look up a configuration value for @racket[key]. If the key is not +present, return @racket[default] (if provided) or raise an error. The +default implementation always raises an error. +} + +@defmethod[(clone [new-section symbol?]) ww-settings%]{ + +Return a “cloned” settings object for a given section or profile. +Used by @racket[ww-webview%] to derive per-window settings from a +shared base. Default implementation raises an error. +} + +@defmethod[(set! [key symbol?] [value any/c]) void?]{ + +Convenience that forwards to @racket[set]. Provided for symmetry with +Racket’s @tt{set!} naming style. +} + + +} diff --git a/sprintf.rkt b/sprintf.rkt new file mode 100644 index 0000000..fbc3337 --- /dev/null +++ b/sprintf.rkt @@ -0,0 +1,114 @@ +(module sprintf racket/base + + (require racket/format + racket/string + ) + + (provide sprintf + sprintf* + ) + + (define re-format + #px"([^%]*)[%]([0-]{0,1})([1-9][0-9]*|[*]){0,1}([.]([0-9]+|[*])){0,1}(l*)([%dfsx])") + + (define-syntax shift + (syntax-rules () + ((_ args) + (let ((first (car args))) + (set! args (cdr args)) + first)))) + + (define (format-part zeros adjust-width precision kind arg) + (if (number? arg) + (let* ((pad-str (if (eq? zeros #f) " " + (if (string=? zeros "0") + "0" + " "))) + (adjust (if (eq? zeros #f) 'right + (if (string=? zeros "-") 'left 'right))) + (min-width (if (eq? adjust-width #f) 1 adjust-width)) + (precision (if (eq? precision #f) 0 + (if (eq? kind 'd) + 0 + precision))) + (base (if (eq? kind 'x) 16 10)) + ) + (when (eq? kind 's) + (error "argument is a number, but string expected")) + (let ((r (~r arg #:pad-string pad-str #:min-width min-width #:precision precision #:base base))) + (if (eq? adjust 'left) + (let ((r-trim (string-trim r))) + (string-append r-trim + (make-string + (- (string-length r) (string-length r-trim)) + #\space))) + r))) + (let* ((pad-str (if (string=? zeros "") " " zeros)) + (min-width (if (eq? adjust-width #f) 0 adjust-width)) + (max-width (if (eq? precision #f) +inf.0 precision)) + (adjust (if (eq? zeros #f) 'left + (if (string=? zeros "-") 'left 'right))) + ) + (unless (eq? kind 's) + (error "argument is a string, but a number is expected")) + (~a arg #:pad-string pad-str #:min-width min-width #:max-width max-width #:align adjust)) + ) + ) + + (define-syntax fmt + (syntax-rules () + ((_ a ...) + (format a ...)))) + + (define (do-format format args) + (if (null? args) + (let ((m (regexp-match re-format format))) + (unless (eq? m #f) + (error (fmt "formatting left, but no arguments left: ~a" format))) + format) + (let ((m (regexp-match re-format format))) + (when (eq? m #f) + (error (fmt "arguments left, but no formatting left: ~a" format))) + (let* ((matched-length (string-length (list-ref m 0))) + (prefix (list-ref m 1)) + (zeros (list-ref m 2)) + (adjust-width (list-ref m 3)) + (precision (list-ref m 5)) + (long (list-ref m 6)) + (kind (string->symbol (list-ref m 7))) + ) + (unless (eq? adjust-width #f) + (set! adjust-width (if (string=? adjust-width "*") + (let ((n (shift args))) + (when (null? args) + (error "* requires >= 2 arguments left")) + (unless (number? n) + (error "* requires a number?")) + n) + (string->number adjust-width)))) + (unless (eq? precision #f) + (set! precision (if (string=? precision "*") + (let ((n (shift args))) + (when (null? args) + (error "* requires >= 2 arguments left")) + (unless (number? n) + (error "* requires a number?")) + n) + (string->number precision)))) + (string-append prefix + (if (eq? kind '%) + "%" + (format-part zeros adjust-width precision kind (shift args))) + (do-format (substring format matched-length) args)))) + ) + ) + + + (define (sprintf format . args) + (do-format format args)) + + (define (sprintf* format args) + (do-format format args)) + + + ) ; end of module \ No newline at end of file