EN /HU | Belépés

Window-k

A window-k egy rubbs-application munkafelületei: keretesek, számozottak, válthatók, mozgathatók, és ha bármekkora méretre ki tudják rakni magukat, méretezhetők és nagyíthatók is, Turbo Vision-stílusban. Ez az oldal a BBS::Window-ról, a window DSL-ről (BBS::WindowSpec), a négy kész window-típusról (Info, Form, MasterDetail, Stream), a ButtonBar segédről és a BBS::Dialogs-ról szól. A window-kban lévő vezérlők a Widgetek oldalon vannak.

Window háromféleképpen

Mód Mikor Példa
kész típus, imperatívan kódból nyitott egyszeri window-k BBS::Windows::Form.open(app, title: ..., fields: [...], on_submit: ...)
kész típus, a window DSL-lel egy application név szerint nyitott window-i window :profile, type: :form do \|w\| ... end, aztán open_window(:profile)
BBS::Window kézzel, vagy egy leszármazott amit a típusok nem fednek le BBS::Window.new(title: ...), layout, add, open_window(win)

A kész típusok a BBS::Window leszármazottai: az első fejezetben leírtak rájuk is érvényesek.

Az oldal minden példája teljes program vagy teljes definíció: a definíciót egy flow call lépéséből indítsd, ahogy az Application oldalon látható, vagy a saját szerveredből.

BBS::Window

A BBS::Window (lib/bbs/window.rb) egy BBS::Container kerettel, címmel, árnyékkal, bezárógombbal és saját fókuszkezelővel. Az application veremben tartja a nyitott window-kat; az utolsó az aktív, az kapja elsőként a bemenetet.

Konstruktor és attribútumok

TEXT
BBS::Window.new(title: nil, modal: false, shadow: true, on_close: nil,
                close_key: :escape, maximizable: false, movable: true)
Attribútum Alapérték Jelentés
title nil szöveg a felső keretben; szóközzel érdemes körülvenni (' Inbox '). Levágódik, ha a gomboknak kell a hely
modal false a modális window elöl marad, amíg be nem zárják: amíg nyitva van, nincs váltás, nincs száma, mindig dupla a kerete. A dialógusok modálisak, a kész típusok nem
shadow true a vetett árnyék (nagyítva sosem rajzolódik)
on_close nil a window-val hívódik, amikor az application bezárja. false eltünteti a [■] bezárógombot (az Esc ettől még bezár)
close_key :escape a billentyű, ami bezárja a window-t, ha egy widget sem vette el; nil: nincs ilyen
maximizable false nagyítógombot ad egy resizable? window-nak
movable true a hívó mozgathatja (és ha resizable?, méretezheti)
number az open_window adja 1–9, a felső keret jobb oldalán; az Alt-N ezt a window-t választja

Mind írható-olvasható attribútum (win.title = ' Új cím '). Az app=-et az open_window állítja be.

Kézzel épített window

A window helyét a layout(x, y, width, height) adja meg (1-től számozott képernyőcellák, a kerettel együtt), a gyerekeit képernyő-koordinátákban helyezd el benne, add hozzá őket, aztán reset_focus, és nyisd meg:

RUBY
require 'bbs'

def open_hello(app)
  win = BBS::Window.new(title: ' Hello ', on_close: ->(_w) { app.context[:closed] = true })
  win.layout(20, 6, 40, 7)
  label = BBS::Widgets::Label.new(text: 'A window built by hand.', style_key: :window_text)
  label.layout(22, 8, 36, 1)
  win.add(label)
  ok = BBS::Widgets::Button.new(label: '&OK', on_click: ->(_b) { app.close_window(win) })
  ok.layout(36, 10, 8, 1)
  win.add(ok)
  win.reset_focus
  app.open_window(win)
end

APP = BBS::Application.define do
  status_hint('F2', 'Hello') { open_hello(self) }
  status_hint('Alt-X', 'Bye') { :halt }
end

A BBS::Dialogs.centred_window(app, width:, height:, title:) egy középre tett, kirakott window-t ad kiindulásnak (modálisat; állítsd modal = false-ra, ha váltható legyen).

Metódusok

Metódus Mit csinál
focus_manager a window BBS::FocusManager-e, első használatkor jön létre
reset_focus új fókuszkezelő az első fókuszálható gyereken; widgetek hozzáadása után hívd
close :close_window-t ad vissza; az ezt visszaadó kezelő hatására az application bezárja a window-t (és egyszer meghívja az on_close-t)
tick(app) nagyjából másodpercenként hívódik, amíg a hívó tétlen; üres, lekérdezéshez írd felül
relayout(app) a terminál átméretezése után hívódik (lásd lent)
shift(dx, dy) eltolja a window-t az összes leszármazottjával
move_to(x, y, width = a mostani, height = a mostani) áthelyezi (és ha resizable?, átméretezi) a window-t, az asztalon belül és legalább 24×7-esre; ettől kezdve a window ezt a helyet tartja
toggle_maximize nagyítás vagy visszaállítás, ha zoomable?
maximized?, busy? nagyítva van; épp mozgatják vagy méretezik
resizable? itt false; azokban a window-kban igaz, amik bármekkora méretre kirakhatók
zoomable?, movable?, sizable? maximizable && resizable?; movable és nincs nagyítva; mozgatható és átméretezhető
normal_rect, place(x, y, w, h) az átméretezhető window-k elrendező horgonyai (lásd lent)

Hogyan kezeli a window a bemenetet

A Window#handle_event sorrendje:

  1. Billentyűzetes mozgatás vagy méretezés közben minden billentyű ahhoz tartozik.
  2. A címsor vagy a sarok egérrel húzása követi az egeret.
  3. Az F5 vagy az Alt-Enter nagyít vagy visszaállít (ha zoomable?).
  4. A Ctrl-F5 vagy az F6 elindítja a billentyűzetes mozgató/méretező módot (ha movable?). Az F6 csak akkor jut ide, ha nem váltott window-t, vagyis ha ez az egyetlen váltható window.
  5. Bal kattintás: a [■]-on bezár, a [↑]/[↕]-on vagy dupla kattintással a címsoron nagyít/visszaállít, a ┘ sarkon méretezésbe, a címsoron mozgatásba kezd; máshol a mutató alatti widgetre teszi a fókuszt.
  6. Az egérgörgő a mutató alatti widgethez megy.
  7. A Tab és a Shift-Tab a fókuszt mozgatja; utána a fókuszált widget kapja az eseményt.
  8. Egy Shift/Ctrl/Alt + nyíl, Home, End, PgUp vagy PgDn, amit senki nem vett el, sima billentyűként újra próbálkozik (a Shift-Le egy listában lefelé lép).
  9. Egy betű vagy Alt+betű, amit senki nem vett el, megnyomja azt az engedélyezett gombot, aminek az a gyorsbillentyűje ('y' vagy Alt-Y a &Yes-re).
  10. A close_key bezárja a window-t.

A 9. pont magyarázza az űrlapok gyorsbillentyű-szabályát: szövegmezőben a betű begépelődik, így csak az Alt+betű jut el a gombokig; jelölőnégyzeten vagy gombon a sima betű is megnyomja őket.

Keret és gombok

TEXT
╔═[■]═  Notes ═══════════════════════════1══[↑]═╗   aktív: dupla keret
║                                               ║
║                                               ║
╚═══════════════════════════════════════════════┘   ┘ = méretező sarok
┌─[■]─  Planets ─────────────────────2──[↑]─┐       inaktív: szimpla keret
Elem Mikor látszik Kattintás
[■] bezárógomb, bal oldalt az on_close nem false, és a window legalább 6 széles bezár
cím mindig húzás mozgat, dupla kattintás nagyít
szám ha a window-nak van, és legalább 20 széles —
[↑] / [↕] nagyítógomb, jobb oldalt zoomable? és legalább 14 széles nagyítás / visszaállítás
┘ sarok sizable? húzás méretez

Az aktív window-nak és minden modális window-nak dupla kerete és fókuszált címstílusa van, a többinek szimpla kerete. Mozgatás vagy méretezés közben a keret kiemelt, és a cím a billentyűket mutatja.

Nagyítás

Az a maximizable window, ami resizable?, nagyítógombot kap. A [↑], az F5, az Alt-Enter vagy egy dupla kattintás a címsoron kitölti vele az asztalt a menüsor és a státuszsor között (app.desktop_rect); a [↕] és ugyanezek a billentyűk visszaállítják. A nagyított window nagyított marad a terminál átméretezésekor, és se nem mozgatható, se nem méretezhető.

Átméretezhető típusok: MasterDetail, Stream, Form (egyetlen, saját :rows nélküli textarea kapja a maradék magasságot), és a scroll: true-s Info. Egy statikus Info nem rakható ki más méretre, így rajta a maximizable-nek nincs hatása. Sok olvasnivalót tartalmazó window-khoz való, nem kis dialógusokhoz.

Mozgatás és méretezés

A movable? (mozgatható és nem nagyított) window mozgatható; ha resizable? is, méretezhető.

  • Egér: a címsor húzása mozgat, a ┘ sarok húzása méretez.
  • Billentyűzet: a Ctrl-F5 (vagy az F6, ha ez az egyetlen window) mozgató módba lép. A nyilak egy cellát mozgatnak, a Shift+nyilak méreteznek, a Tab vált a mozgatás és a méretezés között, az Enter, az Esc vagy a Ctrl-F5 befejezi.
  • Kódból: win.move_to(x, y, width, height).

A window az asztalon marad, és legalább 24×7-es (vagy akkora, mint az asztal, ha az kisebb). Mozgatás vagy méretezés után ezt a téglalapot tartja: a terminál átméretezésekor szükség esetén visszahúzódik a képernyőre, és a nagyításból visszaállítva is ide tér vissza.

Window-k váltása

Egyszerre több window is nyitva lehet; abban a sorrendben takarják egymást, ahogy utoljára használták őket.

  • Minden nem modális window megnyitáskor a legkisebb szabad számot kapja 1 és 9 között; a tizedik már nem kap.
  • Az F6 előrehozza a következő window-t (szám szerint), a Shift-F6 az előzőt, az Alt-1 … Alt-9 az adott számút.
  • Az aktív mögötti window-ra kattintás előrehozza, és a kattintás tovább is megy hozzá: egy gomb, a bezárógomb vagy a címsor húzása azonnal reagál.
  • Az aktív window bezárásakor az alatta lévő lesz aktív.
  • Amíg modális window van nyitva, nincs váltás: előbb a kérdésre kell válaszolni.
  • Kódból: app.raise_window(win), app.cycle_windows(1) / (-1), app.switchable_windows.

A terminál átméretezése

Átméretezéskor az application minden window-n meghívja a relayout(app)-ot:

  • a mozgatott vagy méretezett window visszakerül a saját téglalapjára, a képernyőre húzva;
  • az átméretezhető window nagyítva az app.desktop_rect-re, egyébként a normal_rect-jére kerül;
  • minden más window megtartja a méretét, és újra középre kerül.

Saját, átméretezhető window

Egy leszármazott három metódussal kapcsolódik be a nagyításba, a méretezésbe és az átméretezés kezelésébe: resizable? (igaz), normal_rect (a szokásos [x, y, width, height]-je az adott terminálon) és place(x, y, width, height) (kirakja magát és a gyerekeit arra a téglalapra). A tick szívverést ad neki:

RUBY
require 'bbs'

class ClockWindow < BBS::Window
  def initialize(app)
    super(title: ' Clock ', maximizable: true)
    self.theme = app.theme
    @app   = app
    @label = BBS::Widgets::Label.new(text: now, style_key: :text_bright, align: :center)
    add(@label)
    place(*normal_rect)
  end

  def resizable? = true

  def normal_rect
    w = 30
    h = 5
    [(@app.cols - w) / 2 + 1, (@app.rows - h) / 2 + 1, w, h]
  end

  def place(x, y, width, height)
    layout(x, y, width, height)
    @label.layout(x + 2, y + height / 2, width - 4, 1)
  end

  def tick(_app) = (@label.text = now)

  private

  def now = Time.now.strftime('%H:%M:%S')
end

APP = BBS::Application.define do
  status_hint('F2', 'Clock') { open_window(ClockWindow.new(self)) }
  status_hint('Alt-X', 'Bye') { :halt }
end

Az óra középen nyílik, az F5 kitölti vele a képernyőt, a Ctrl-F5 és a Shift+nyilak nagyobbá teszik, és minden tétlen másodpercben frissül.

A window DSL

Az Application.define-on belül a window(name, type:) { |w| ... } egy BBS::WindowSpec-et regisztrál (lib/bbs/window_spec.rb); az app.open_window(name, **extras) nyitja meg (a WindowSpec#open(app, **extras)-en át, ez a spec egyetlen megnyitási módja). A spec minden megnyitáskor újra kiértékeli az attribútumait, így azok függhetnek a hívótól, a képernyőmérettől vagy az extráktól.

RUBY
require 'bbs'

Board = Struct.new(:id, :title)

APP = BBS::Application.define do
  window :compose, type: :form do |w|
    w.title { |ctx| " Post to #{ctx[:board].title} " }
    w.width 70
    w.field :body, type: :textarea, max_length: 1000
    w.on_submit do |values, form, ctx|
      if values[:body].strip.empty?
        form.error('The message is empty.')
        :keep
      else
        ctx[:app].context[:last_post] = [ctx[:board].id, values[:body]]
        :close
      end
    end
  end

  status_hint('F3', 'Post') { open_window(:compose, board: Board.new(1, 'General')) }
end

A ctx

A spec minden blokkja ctx-et kap:

TEXT
ctx = { app: app, **app.context, **app.services, **extras }

Azonos kulcsnál a későbbi forrás nyer: az extra felülírja a service-t, a service a context-kulcsot. A ctx minden megnyitáskor új; a maradandó állapotot a ctx[:app].context-be írd.

Értékek és blokkok

Minden attribútum értéket vagy blokkot kap:

TEXT
w.width 70                              # érték
w.width { |ctx| ctx[:app].cols - 10 }   # minden megnyitáskor kiértékelve, ctx-szel
w.width = 70                            # a skalár attribútumoknak setterük is van

A blokkal megadott érték-attribútumok csak a ctx-et kapják. A callback-attribútumok (item_label, render_detail, on_submit, ...) később hívódnak, a saját argumentumaikkal, utolsóként a ctx-szel.

Attribútumok típusonként

Attribútum info form master_detail stream Megjegyzés
title ✓ ✓ ✓ ✓
width, height ✓ ✓ ✓ ✓ az alapérték típusonként más
maximizable ✓ ✓ ✓ ✓ nagyítógomb; statikus infón hatástalan
body ✓ a törzs elemei
scroll ✓ egyetlen görgethető törzs
padding ✓ az űrlapnak nem adódik át
button(**opts) { \|win, ctx\| } ✓ hívásonként egy gomb
field(key, **opts) ✓ hívásonként egy mező
prefill { \|ctx\| hash } ✓ értékek a saját value: nélküli mezőknek
hint, submit_label, cancel_label ✓
on_submit \|values, form, ctx\| \|text, app, ctx\| elhagyható; nélküle az űrlap Save gombja csak bezár
on_cancel { \|form, ctx\| } ✓
items { \|ctx\| } ✓ a lista
item_label { \|item, ctx\| } ✓
render_detail { \|item, width, ctx\| } ✓
bottom_meta { \|item, ctx\| } ✓
on_activate { \|item, win, ctx\| } ✓ Enter egy elemen
list_width, empty_message ✓
action(**opts) { \|win, ctx\| } ✓ hívásonként egy gomb
close_button, close_label ✓ ✓
initial_lines { \|ctx\| } ✓
placeholder, buffer_size ✓
on_poll { \|app, ctx\| } ✓

Az olyan attribútumot, amit az adott típus nem használ, a spec csendben figyelmen kívül hagyja. Az 1.0 óta a type: :form specnek nem kell on_submit: ha nincs, a Save egyszerűen bezárja az űrlapot.

Lent minden típusnál az imperatív forma mellett ott a DSL-es is. A négy típus a kötelező kulcsszavait kéri (app:, title:, az űrlap fields:-ét, a master-detail items:, item_label: és render_detail: kulcsszavát), a többit opcióként, amelyeket a típus OPTIONS konstansa ellenőriz; az alapértékek a lenti táblázatokban vannak, ismeretlen opcióra ArgumentError jön.

BBS::Windows::Info

Középre tett window üzenetekhez, nyitóképernyőkhöz és kulcs–érték összefoglalókhoz (lib/bbs/windows/info.rb).

TEXT
BBS::Windows::Info.open(app, title:, body: [], buttons: nil, width: 60, height: nil,
                        scroll: false, padding: nil, maximizable: false)
Opció Alapérték Jelentés
title — (kötelező) a cím
body [] a törzs elemei, lásd lent
buttons nil gombleírók (lásd lent: BBS::Windows::ButtonBar); nil egy középre tett [ Close ]-t ad, [] egyet sem
width 60 legfeljebb a képernyő szélessége mínusz 4
height természetes padding + a törzs sorai + 2 a gomboknak; legfeljebb a képernyő magassága mínusz 4; scroll: true esetén a törzs 10 sornak számít
scroll false true: az egész törzs egyetlen ScrollView lesz (átméretezhető, nagyítható)
padding { sides: 2, top: 1, bottom: 2 } bármelyik a háromból felülírható
maximizable false csak scroll: true mellett

A törzs elemei:

Elem Mit mutat
'szöveg' egy sor :window_text stílusban (görgetett módban a string ANSI-t is tartalmazhat)
:spacer, '', nil üres sor
:rule vízszintes vonal
{ text:, style:, align: } egy sor egy témastílusban (:accent, :text_dim, ...), align: :left / :center / :right (görgetett módban nem)
{ kv: [[kulcs, érték], ...], label_width: } kulcs–érték sorok; a kulcsoszlop alapértelmezése a leghosszabb kulcs + 1, 8 és 16 között
{ widget:, height: } bármilyen widget, height sorban (görgetett módban nem)
egy BBS::Widget a widget úgy, ahogy van, a saját magasságával (görgetett módban nem)

Görgetett módban a widget-elemek ArgumentError-t dobnak. A fókusz a primary: gombon indul, ha van ilyen. A scroll_view görgetett módban a ScrollView-t adja vissza.

RUBY
require 'bbs'

def open_info(app)
  BBS::Windows::Info.open(app,
    title: ' System ',
    width: 50,
    body: [
      { text: 'Notes desk', style: :accent, align: :center },
      :spacer,
      { kv: [['Ruby', RUBY_VERSION], ['Screen', "#{app.cols}x#{app.rows}"]] },
      :rule,
      'Plain text line.'
    ],
    buttons: [{ label: '&OK', primary: true }])
end

APP = BBS::Application.define do
  status_hint('F2', 'Info') { open_info(self) }
end
TEXT
               ╔═[■]═  System ═══════════════════════════════1══╗
               ║                   Notes desk                   ║
               ║                                                ║
               ║ Ruby      3.3.1                                ║
               ║ Screen    80x25                                ║
               ║ ────────────────────────────────────────────── ║
               ║ Plain text line.                               ║
               ║                                                ║
               ║                                                ║
               ║                     [ OK ]                     ║
               ╚════════════════════════════════════════════════╝

Ugyanez a DSL-lel, plusz egy hosszú, görgethető:

RUBY
require 'bbs'

APP = BBS::Application.define do
  window :system, type: :info do |w|
    w.title { |ctx| " #{ctx[:app].cols}x#{ctx[:app].rows} " }
    w.width 44
    w.body do |ctx|
      [{ text: 'Notes desk', style: :accent, align: :center }, :spacer,
       { kv: [['Caller', ctx[:username]], ['Ruby', RUBY_VERSION]] }]
    end
    w.button(label: '&OK', primary: true)
    w.button(label: '&Rules') { |_win, ctx| ctx[:app].open_window(:rules) }
  end

  window :rules, type: :info do |w|
    w.title       ' House rules '
    w.scroll      true
    w.maximizable true
    w.height      12
    w.body        { |_ctx| (1..30).map { |i| "#{i}. Be nice." } }
  end

  status_hint('F2', 'System') { open_window(:system) }
  status_hint('F3', 'Rules') { open_window(:rules) }
end

A &Rules bezárja a rendszer-window-t, és megnyitja a szabályokat: a gomb bezárja a window-ját, kivéve, ha a blokkja :keep-et ad vissza, vagy a gombon keep_open: true áll.

BBS::Windows::Form

Űrlap: egymás alatti, címkézett mezők, fölöttük egy opcionális tippsor, egy státuszsor az ellenőrzési üzeneteknek, és egy Save / Cancel gombpár (lib/bbs/windows/form.rb).

TEXT
BBS::Windows::Form.open(app, title:, fields:, on_submit: nil, hint: nil, width: 60, height: nil,
                        submit_label: '&Save', cancel_label: '&Cancel',
                        padding: nil, on_cancel: nil, maximizable: false)
Opció Alapérték Jelentés
title — (kötelező) a cím
fields — (kötelező) mezőleírók, lásd lent
on_submit nil ->(values, form) { ... }; :keep esetén nyitva marad, bármi más bezárja. nil: a Save csak bezár
on_cancel nil ->(form) { ... }, a Cancel gomb futtatja a window bezárása előtt
hint nil egy sor a mezők fölött, :input_label stílusban
width 60 legfeljebb a képernyő szélessége mínusz 4
height természetes a mezők magasságának összege + a fix sorok; explicit magasságnál egyetlen :rows nélküli textarea nő
submit_label, cancel_label '&Save', '&Cancel' a gombok felirata; a & jelöli az Alt-gyorsbillentyűt
padding { sides: 2, top: 1, bottom: 2 } belső margók
maximizable false a nagyítógomb (az űrlapok átméretezhetők)

A mezőleíró kulcsai:

Kulcs Típusok Alapérték Jelentés
key mind — a kulcs a values-ban
type — :input :input, :textarea vagy :checkbox
label input, checkbox nil input: a címke balra (az oszlop a leghosszabb címke + 1, 8 és 16 között); checkbox: a szöveg az [x] után; textareánál figyelmen kívül marad
value mind '' / nincs bejelölve a kezdőérték; checkboxnál bármely igaz érték bejelöli
max_length input, textarea 256 / 8000 a karakterek maximális száma
placeholder input '' szürke szöveg, amíg üres
password input false a karakterek helyett *-ot mutat
rows textarea 6 a textarea magassága

A values mezőkulcs szerinti Hash: inputnál és textareánál string (a sorok \n-nel összefűzve), checkboxnál true/false.

Metódus Mit csinál
form.values a pillanatnyi értékek
form.error(text) a státuszsor :error stílusban
form.info(text) a státuszsor :input_label stílusban
form.info('') kiüríti a státuszsort
form.status_label a státuszsor Label widgete

Billentyűk az űrlapon. A fókusz az első mezőn indul, a Tab és a Shift-Tab lép a mezők és a gombok között. Az Enter szövegmezőben nem küldi el az űrlapot (nem csinál semmit; textareában új sort kezd): a hívó a Save gombbal küld, vagyis &Save esetén Alt-S-sel, vagy Tabbal odalép és Entert üt. Az Alt-C vagy az Esc megszakít; az Esc az on_cancel meghívása nélkül zár be. Jelölőnégyzeten a Szóköz vagy az Enter átbillenti, és egy sima s már megnyomja a Save-et. Írd bele a hint-be („Alt-S saves.”).

RUBY
require 'bbs'

def open_profile(app)
  BBS::Windows::Form.open(app,
    title: ' Profile ',
    width: 56,
    hint: 'Alt-S saves, Esc closes.',
    fields: [
      { key: :name,   label: 'Name',     value: app.context[:username], max_length: 20 },
      { key: :pin,    label: 'PIN',      password: true, max_length: 4, placeholder: '4 digits' },
      { key: :public, label: 'Show me in Who is online', type: :checkbox, value: true },
      { key: :bio,    type: :textarea, rows: 4, max_length: 500 }
    ],
    on_submit: lambda do |values, form|
      unless values[:pin].match?(/\A\d{4}\z/)
        form.error('The PIN is 4 digits.')
        return :keep
      end
      app.context[:profile] = values
      :close
    end,
    on_cancel: ->(_form) { app.context[:profile_cancelled] = true })
end

APP = BBS::Application.define do
  status_hint('F2', 'Profile') { open_profile(self) }
end

Ha a PIN-be 12-t írunk, és Alt-S-t nyomunk:

TEXT
            ╔═[■]═  Profile ════════════════════════════════════1══╗
            ║ Alt-S saves, Esc closes.                             ║
            ║                                                      ║
            ║ Name      Ann                                        ║
            ║                                                      ║
            ║ PIN       **_                                        ║
            ║                                                      ║
            ║ [x] Show me in Who is online                         ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║                                                      ║
            ║ The PIN is 4 digits.                                 ║
            ║                                   [ Save ] [ Cancel ]║
            ╚══════════════════════════════════════════════════════┘

A DSL-es forma. A prefill azoknak a mezőknek ad értéket, amiknek nincs saját value:-juk; egy mező value:-ja ctx-et kapó blokk is lehet:

RUBY
require 'bbs'

APP = BBS::Application.define do
  window :profile, type: :form do |w|
    w.title        ' Profile '
    w.width        56
    w.hint         'Alt-S saves, Esc closes.'
    w.field :name, label: 'Name', max_length: 20
    w.field :pin,  label: 'PIN', password: true, max_length: 4
    w.field :public, label: 'Show me in Who is online', type: :checkbox
    w.field :since, label: 'Since', value: ->(ctx) { ctx[:app].context[:since] || Time.now.year.to_s }
    w.prefill { |ctx| { name: ctx[:username], public: true } }
    w.submit_label '&Save'
    w.on_submit do |values, form, ctx|
      if values[:pin].match?(/\A\d{4}\z/)
        ctx[:app].context[:profile] = values
        :close
      else
        form.error('The PIN is 4 digits.')
        :keep
      end
    end
    w.on_cancel { |_form, ctx| ctx[:app].context[:profile_cancelled] = true }
  end

  status_hint('F2', 'Profile') { open_window(:profile) }
end

BBS::Windows::MasterDetail

Kétpaneles böngésző: balra egy lista, jobbra a kijelölt elem részletei, opcionálisan egy meta-sor és egy gombsor (lib/bbs/windows/master_detail.rb). A listában lépkedve a részletek újrarajzolódnak.

TEXT
BBS::Windows::MasterDetail.open(app, title:, items:, item_label:, render_detail:,
                                list_width: nil, bottom_meta: nil, actions: nil, on_activate: nil,
                                close_button: true, close_label: '&Close',
                                empty_message: '  (empty)', width: nil, height: nil, maximizable: false)
Opció Alapérték Jelentés
title — (kötelező) a cím
items — (kötelező) bármiből álló Array
item_label — (kötelező) ->(item) { szöveg }, egy listasor
render_detail — (kötelező) ->(item, width) { sorok }: stringek Array-e (ANSI megengedett) a részletpanelnek, a width a használható szélesség. Megnyitáskor, minden kijelölés-váltáskor és átméretezés után hívódik
list_width a window harmada, 20–40 a listaoszlop, legfeljebb a window fele
bottom_meta nil ->(item) { szöveg }: egy sor a panelek alatt
actions nil további gombleírók (lásd lent: BBS::Windows::ButtonBar), a Close elé kerülnek
on_activate nil ->(item, window) { ... }, Enterre hívódik a listában
close_button true egy Close gomb a végére
close_label '&Close' a felirata
empty_message ' (empty)' a részletpanel szövege, ha üres a lista
width, height képernyő − 4, képernyő − 6 a méret
maximizable false a nagyítógomb (a master-detail window-k átméretezhetők)
Tag Mit csinál
replace_items(items, title: nil) helyben lecseréli a listát, kijelöli az első elemet, és ha kell, a címet is átírja: navigáció egy fájlböngészőben vagy egy wikiben
reload_detail(keep_scroll: false) újrarajzolja a kijelölt elem részleteit
list_widget a ListBox; a selected_item a kijelölt elem
detail_view a jobb oldali ScrollView
meta_label a meta-sor Label-je, vagy nil

A fókusz a listában indul. A Fel/Le, a PgUp/PgDn, a Home/End és a görgő a listában mozog; a Tab a gombokra lép. Több gomb jobbra igazodik, a magában álló Close középre.

RUBY
require 'bbs'

Planet = Struct.new(:name, :moons, :about)
PLANETS = [Planet.new('Mercury', 0, 'Closest to the Sun.'),
           Planet.new('Earth', 1, 'Home.'),
           Planet.new('Mars', 2, 'Red and dusty.')].freeze

def open_planets(app)
  BBS::Windows::MasterDetail.open(app,
    title: ' Planets ',
    items: PLANETS,
    item_label: ->(p) { p.name },
    render_detail: ->(p, width) { [p.name.upcase, '', *BBS::FrameBuffer.wrap_ansi(p.about, width)] },
    bottom_meta: ->(p) { "#{p.moons} moon(s)" },
    list_width: 20,
    width: 60, height: 14,
    maximizable: true,
    on_activate: ->(p, _win) { BBS::Dialogs.message(app, "You picked #{p.name}.") },
    actions: [
      { label: '&Rocky only', keep_open: true,
        on_click: ->(win) { win.replace_items(PLANETS.reject { |p| p.name == 'Earth' }, title: ' Rocky ') } }
    ])
end

APP = BBS::Application.define do
  status_hint('F2', 'Planets') { open_planets(self) }
end
TEXT
          ╔═[■]═  Planets ════════════════════════════════════1══[↑]═╗
          ║                                                          ║
          ║   Mercury             EARTH                              ║
          ║   Earth                                                  ║
          ║   Mars                Home.                              ║
          ║                                                          ║
          ...
          ║ 1 moon(s)                                                ║
          ║                                  [ Rocky only ] [ Close ]║
          ╚══════════════════════════════════════════════════════════╝

A DSL-es forma; az items megnyitáskor értékelődik ki, a callbackek utolsóként a ctx-et kapják:

RUBY
require 'bbs'

Planet = Struct.new(:name, :moons, :about)

APP = BBS::Application.define do
  services planets: [Planet.new('Mercury', 0, 'Closest to the Sun.'),
                     Planet.new('Earth', 1, 'Home.'),
                     Planet.new('Mars', 2, 'Red and dusty.')]

  window :planets, type: :master_detail do |w|
    w.title         ' Planets '
    w.maximizable   true
    w.width         60
    w.height        14
    w.list_width    20
    w.items         { |ctx| ctx[:planets] }
    w.item_label    { |p, _ctx| p.name }
    w.render_detail { |p, width, _ctx| [p.name.upcase, '', *BBS::FrameBuffer.wrap_ansi(p.about, width)] }
    w.bottom_meta   { |p, _ctx| "#{p.moons} moon(s)" }
    w.empty_message '  (no planets)'
    w.on_activate   { |p, _win, ctx| BBS::Dialogs.message(ctx[:app], "You picked #{p.name}.") }
    w.action(label: '&Rocky only', keep_open: true) do |win, ctx|
      win.replace_items(ctx[:planets].reject { |p| p.name == 'Earth' }, title: ' Rocky ')
    end
  end

  status_hint('F2', 'Planets') { open_window(:planets) }
end

A render_detail megkapja a panel szélességét; a szöveget a BBS::FrameBuffer.wrap_ansi(text, width)-del tördeld rá, vagy rendereld Markdownként a BBS::Markdown.to_lines(text, width: width, theme: theme)-mel (lásd Rajzolás).

BBS::Windows::Stream

Élő nézet: görgethető előzmények, alattuk egy beviteli sor, és lekérdezés minden tétlen tickben (lib/bbs/windows/stream.rb). Chat- és log-window-khoz készült.

TEXT
BBS::Windows::Stream.open(app, title:, initial_lines: [], placeholder: '',
                          on_submit: nil, on_poll: nil, buffer_size: 500,
                          width: nil, height: nil, close_button: true, close_label: '&Close',
                          maximizable: false)
Opció Alapérték Jelentés
title — (kötelező) a cím
initial_lines [] a kezdő előzmények (ANSI megengedett)
placeholder '' szürke szöveg az üres beviteli sorban
on_submit nil ->(text, app) { ... } Enterre, nem üres sornál; a beviteli sor kiürül, hacsak :keep-et nem ad vissza
on_poll nil ->(app) { sorok } minden tétlen tickben; a visszaadott sorok (vagy nil / []) a végére kerülnek, és az előzmények a végére görgetnek
buffer_size 500 ennyi sort tart meg legfeljebb
width, height képernyő − 8, képernyő − 6 a méret
close_button, close_label true, '&Close' középre tett Close gomb
maximizable false a nagyítógomb (a streamek átméretezhetők)

A history a ScrollView-t, az input a TextInput-ot adja vissza. A fókusz a beviteli sorban indul.

Az on_submit nem teszi be a sort az előzmények közé: a window csak azt mutatja, amit az initial_lines és az on_poll ad neki. Küldd el a sort a service-nek, és a következő lekérdezés hozza vissza (így a hívó ugyanabban a sorrendben látja, mint mindenki más), vagy fűzd hozzá magad: win.history.lines += [line].

RUBY
require 'bbs'

def open_log(app)
  BBS::Windows::Stream.open(app,
    title: ' Log ',
    initial_lines: ['Log opened.'],
    placeholder: 'Type a line, Enter adds it',
    buffer_size: 200,
    on_submit: ->(text, _app) { (app.context[:log] ||= []) << text },
    on_poll: ->(_app) { (app.context[:log] || []).shift(10).map { |l| "> #{l}" } })
end

APP = BBS::Application.define do
  status_hint('F2', 'Log') { open_log(self) }
end

Egy chat a DSL-lel, közös service-szel. Az initial_lines feljegyzi, meddig olvasott a hívó, az on_poll hozza, ami azóta jött:

RUBY
require 'bbs'

class Chat
  def initialize
    @lines = []
    @mutex = Mutex.new
  end

  def history = @mutex.synchronize { @lines.last(50) }
  def say(who, text) = @mutex.synchronize { @lines << "#{who}: #{text}" }
  def since(n) = @mutex.synchronize { @lines.drop(n) }
  def size = @mutex.synchronize { @lines.size }
end

APP = BBS::Application.define do
  services chat: Chat.new

  window :chat, type: :stream do |w|
    w.title         ' Chat '
    w.maximizable   true
    w.initial_lines do |ctx|
      ctx[:app].context[:chat_seen] = ctx[:chat].size
      ctx[:chat].history
    end
    w.placeholder   'Say something, Enter sends'
    w.buffer_size   300
    w.on_submit     { |text, _app, ctx| ctx[:chat].say(ctx[:username], text) }
    w.on_poll do |_app, ctx|
      seen  = ctx[:app].context[:chat_seen]
      fresh = ctx[:chat].since(seen)
      ctx[:app].context[:chat_seen] = seen + fresh.size
      fresh
    end
  end

  status_hint('F4', 'Chat') { open_window(:chat) }
end

Lekérdezés csak akkor fut, ha a hívó tétlen, nagyjából másodpercenként; aki gépel, az az új sorokat a szünetekben látja.

BBS::Windows::ButtonBar

Ez a segéd rakja ki a kész window-k gombsorait, és a saját window-kon is használható (lib/bbs/windows/button_bar.rb). A gombok az alsó keret fölötti sorba kerülnek.

Függvény Mit csinál
ButtonBar.attach(window, app, descriptors) létrehozza, elhelyezi és a window-hoz adja a gombokat; { widgets:, primary:, by_key: }-t ad vissza
ButtonBar.place(window, widgets, descriptors) a meglévő gombokat újra elhelyezi a window aktuális határaihoz (a place-ből hívd)

A gombleíró (ugyanaz az Info buttons:-ában, a MasterDetail actions:-ében, a w.button-ben és a w.action-ben):

Kulcs Alapérték Jelentés
label — a szöveg; a & jelöli a gyorsbillentyűt (betű vagy Alt+betű)
on_click nil ->(window) { ... }; a DSL-ben a blokk, \|win, ctx\|
keep_open false true: kattintás után nyitva marad a window
primary false ez a gomb kapja a fókuszt, amikor egy Info megnyílik (az attach primary:-ként adja vissza)
cancel false csak tájékoztató; az Esc úgyis bezár a window close_key-jén át
align egy gombnál :center, többnél :right :left a bal széltől sorakoztat, :right a jobb szélhez simít deklarálási sorrendben, :center középre tesz
key nil név a by_key-hez

Az on_click után a window bezárul, hacsak nincs keep_open, vagy a blokk nem :keep-et adott. A visszatérési érték máshová nem jut el: egy gomb :halt-ja nem fejezi be az applicationt (arra az app.stop! való), az app.open_door(name) viszont egy gombból is kilép a dooron át, mert maga állítja le a ciklust.

RUBY
require 'bbs'

def open_vote(app)
  win = BBS::Dialogs.centred_window(app, width: 44, height: 7, title: ' Vote ')
  win.modal = false
  question = BBS::Widgets::Label.new(text: 'Pineapple on pizza?', style_key: :window_text)
  question.layout(win.bounds.x + 2, win.bounds.y + 2, 40, 1)
  win.add(question)
  bar = BBS::Windows::ButtonBar.attach(win, app, [
    { key: :help, label: '&Help', align: :left, keep_open: true,
      on_click: ->(_w) { BBS::Dialogs.message(app, 'Pick one.') } },
    { key: :yes, label: '&Yes', primary: true, on_click: ->(_w) { app.context[:vote] = :yes } },
    { key: :no,  label: '&No',  on_click: ->(_w) { app.context[:vote] = :no } }
  ])
  win.reset_focus
  win.focus_manager.focus(bar[:primary])
  app.open_window(win)
end

APP = BBS::Application.define do
  status_hint('F2', 'Vote') { open_vote(self) }
end
TEXT
                  ╔═[■]═  Vote ═══════════════════════════1══╗
                  ║                                          ║
                  ║ Pineapple on pizza?                      ║
                  ║                                          ║
                  ║                                          ║
                  ║ [ Help ]                   [ Yes ] [ No ]║
                  ╚══════════════════════════════════════════╝

BBS::Dialogs

Kis modális window-k a gyakori kérdésekhez (lib/bbs/dialogs.rb). Mind azonnal megnyílik, és a window-t adja vissza; egyik sem blokkol: a válasz egy callbackben érkezik. Amíg egy dialógus nyitva van, máshová nem lehet váltani.

Függvény Alapérték Mit csinál
Dialogs.message(app, text, title: ' Message ', width: nil) szélesség: a szöveg 50-re tördelve, legalább 30 a szöveg és egy OK gomb
Dialogs.confirm(app, text, title: ' Confirm ', on_yes: nil, on_no: nil) legalább 36 széles a szöveg, Yes és No
Dialogs.input(app, prompt:, title: ' Input ', value: '', width: 50, on_submit: nil) egy kérdés, egy szövegmező, OK és Cancel
Dialogs.centred_window(app, width:, height:, title: nil) egy modális, középre tett, kirakott Window, még nem megnyitva
Dialógus Válaszok
message az OK, az o, az Alt-O, Enter az OK-n vagy az Esc bezárja
confirm a Yes (y, Alt-Y) bezár és meghívja az on_yes-t; a No (n, Alt-N) bezár és meghívja az on_no-t; az Esc egyiket sem hívja meg
input Enter a mezőben vagy az OK bezár és meghívja az on_submit-ot a szöveggel; a Cancel vagy az Esc hívás nélkül zár be

A confirm callbackjeinek visszatérési értéke eljut a futási ciklusig, így egy helperből az on_yes: -> { open_door(:x) } kilép a dooron át. A hosszú szövegek tördelődnek; width: megadásakor a message width - 6-ra tördel. Saját szöveget a BBS::FrameBuffer.wrap_ansi(text, width)-del tördelj: ez a rubbs egyetlen sortördelője, és ismeri az ANSI-t.

RUBY
require 'bbs'

def ask_delete(app)
  BBS::Dialogs.confirm(app, 'Delete every note?',
    on_yes: -> { BBS::Dialogs.message(app, 'Deleted.', title: ' Done ') },
    on_no:  -> { app.context[:kept] = true })
end

def ask_name(app)
  BBS::Dialogs.input(app, prompt: 'New name:', value: app.context[:username].to_s,
    on_submit: ->(value) { app.context[:username] = value })
end

APP = BBS::Application.define do
  status_hint('F2', 'Delete') { ask_delete(self) }
  status_hint('F3', 'Rename') { ask_name(self) }
  status_hint('F4', 'Hello') { BBS::Dialogs.message(self, "Hello, #{context[:username]}!", width: 40) }
end
TEXT
                         ╔═[■]═  Done ════════════════╗
                         ║ Deleted.                   ║
                         ║                            ║
                         ║                            ║
                         ║           [ OK ]           ║
                         ╚════════════════════════════╝

Egynél több mezőhöz használj Form-ot.

Billentyűk és egér a window-kban

Billentyű / művelet Mit csinál
Tab / Shift-Tab következő / előző vezérlő
Enter / Szóköz megnyomja a fókuszált gombot; átbillenti a jelölőnégyzetet
Enter listában egy MasterDetail on_activate-je
Enter szövegmezőben Formban semmi; Streamben és a Dialogs.input-ban elküld
Alt+betű / betű megnyomja az azzal jelölt gombot (a sima betű csak akkor, ha a fókuszált vezérlő nem fogad betűt)
Esc bezárja a window-t (close_key)
F5, Alt-Enter nagyítás és vissza
Ctrl-F5 mozgató mód: a nyilak mozgatnak, a Shift+nyilak méreteznek, a Tab vált, az Enter / Esc befejezi
F6 / Shift-F6, Alt-1…9 window-váltás
kattintás a [■]-ra / a [↑] [↕]-ra bezárás / nagyítás, visszaállítás
a cím / a ┘ húzása mozgatás / méretezés
dupla kattintás a címen nagyítás és vissza
görgő görgeti a mutató alatti listát vagy szöveget

Lásd még

  • Application — open_window, az eseménykézbesítés, a menüsor és a státuszsor
  • Widgetek — Label, Button, TextInput, TextArea, Checkbox, ListBox, ScrollView, és saját widget írása
  • Rajzolás — a stíluskulcsok (:window_text, :accent, :error, ...), a FrameBuffer.wrap_ansi, Markdown
  • Telnet és a store — a billentyű-szimbólumok és az egér