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), aButtonBarsegédről és aBBS::Dialogs-ról szól. A window-kban lévő vezérlők a Widgetek oldalon vannak.
| 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.
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.
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.
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:
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ó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) |
A Window#handle_event sorrendje:
zoomable?).movable?). Az F6 csak akkor jut ide, ha nem váltott window-t, vagyis ha ez az egyetlen váltható window.[■]-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.'y' vagy Alt-Y a &Yes-re).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.
╔═[■]═ 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.
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.
A movable? (mozgatható és nem nagyított) window mozgatható; ha resizable? is, méretezhető.
┘ sarok húzása méretez.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.
Egyszerre több window is nyitva lehet; abban a sorrendben takarják egymást, ahogy utoljára használták őket.
app.raise_window(win), app.cycle_windows(1) / (-1), app.switchable_windows.Átméretezéskor az application minden window-n meghívja a relayout(app)-ot:
app.desktop_rect-re, egyébként a normal_rect-jére kerül;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:
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.
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.
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 spec minden blokkja ctx-et kap:
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.
Minden attribútum értéket vagy blokkot kap:
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ú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.
Középre tett window üzenetekhez, nyitóképernyőkhöz és kulcs–érték összefoglalókhoz (lib/bbs/windows/info.rb).
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.
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
╔═[■]═ System ═══════════════════════════════1══╗
║ Notes desk ║
║ ║
║ Ruby 3.3.1 ║
║ Screen 80x25 ║
║ ────────────────────────────────────────────── ║
║ Plain text line. ║
║ ║
║ ║
║ [ OK ] ║
╚════════════════════════════════════════════════╝
Ugyanez a DSL-lel, plusz egy hosszú, görgethető:
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.
Ű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).
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.”).
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:
╔═[■]═ 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:
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
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.
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.
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
╔═[■]═ 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:
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).
É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.
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].
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:
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.
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.
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
╔═[■]═ Vote ═══════════════════════════1══╗
║ ║
║ Pineapple on pizza? ║
║ ║
║ ║
║ [ Help ] [ Yes ] [ No ]║
╚══════════════════════════════════════════╝
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.
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
╔═[■]═ Done ════════════════╗
║ Deleted. ║
║ ║
║ ║
║ [ OK ] ║
╚════════════════════════════╝
Egynél több mezőhöz használj Form-ot.
| 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 |
open_window, az eseménykézbesítés, a menüsor és a státuszsor:window_text, :accent, :error, ...), a FrameBuffer.wrap_ansi, Markdown