norcal 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/bin/norcal +217 -208
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01d60640ac4be6724791dd1a510ca21f17748c5cebb705c2acc33864353ee3f2
4
- data.tar.gz: 6f393645e5b43e5671d71acbfdb132a32aa9add25524e725ae8f25a878879b7d
3
+ metadata.gz: bfb181fb12eccf2f5a2a573755d884474dc74eebddf4391fd6a88c5adc55ec7d
4
+ data.tar.gz: a6fe09085766141d71509c87c8ff79aec265453c1456eac18646112a39fe599e
5
5
  SHA512:
6
- metadata.gz: c181d09cbb95b52c24e220c9dc3293fe738d2614cff48d1e65766b4ef18aa2a48e33c8998fb1a420a4d2b12633fd1f2abb881b6501cb597beca45c5ff3ac7cfb
7
- data.tar.gz: 331b2f5093a691edb71a32b36c906b0c76471baf0cfc38555bc540935ede4d8c88bb73381f815110f59bb7f31bb9bd29e119097c998c41d1de62de8a3ef8898b
6
+ metadata.gz: de5b45e2e61b174ec3a3e7b1910f0f50eb063ebc3deeecbe74996750a942aa50ed8d4d46e60bea7be0a47a5cdcddbce9e8433ffd596d7e7eeee031a500b48178
7
+ data.tar.gz: f832d1ff523264fdf967f3ce98cd00bb8f77ada069d3b5ec6df432bc36b08cebe90c851824c2df22380cd381842c7af9aca63c23945c8750711d921804de965a
data/README.md CHANGED
@@ -31,7 +31,7 @@ gem install norcal
31
31
  git clone https://github.com/baosen/norcal.git
32
32
  cd norcal
33
33
  gem build norcal.gemspec
34
- gem install norcal-1.1.0.gem
34
+ gem install norcal-1.3.0.gem
35
35
  ```
36
36
 
37
37
  ## Usage
@@ -49,7 +49,8 @@ norcal --light 2026 # light mode, specific year
49
49
  - ISO week numbers, Monday-first weeks
50
50
  - Sundays and public holidays in red, Saturdays in gray
51
51
  - Easter-based movable holidays (Computus algorithm)
52
- - Notable dates: royal birthdays, Samefolkets dag, Morsdag, Farsdag, solverv, sommertid, and more
52
+ - Notable dates: royal birthdays, Samefolkets dag, Morsdag, Farsdag, solverv, sommertid, advent, and more
53
+ - Filter to show only red days (public holidays)
53
54
  - Today highlighted with yellow background
54
55
  - Dark mode (default) and light mode, with toggle button
55
56
  - Zoom in/out (`+`/`–` buttons, Ctrl+scroll, Ctrl++/Ctrl+-)
data/bin/norcal CHANGED
@@ -61,13 +61,13 @@ end
61
61
  def red_days(year)
62
62
  e = easter(year)
63
63
  {
64
- Date.new(year, 1, 1) => "Nyttårsdag",
64
+ Date.new(year, 1, 1) => "Første nyttårsdag",
65
65
  e - 3 => "Skjærtorsdag",
66
66
  e - 2 => "Langfredag",
67
67
  e => "1. påskedag",
68
68
  e + 1 => "2. påskedag",
69
69
  Date.new(year, 5, 1) => "Arbeidernes dag",
70
- e + 39 => "Kr. Himmelfartsdag",
70
+ e + 39 => "Kristi himmelfartsdag",
71
71
  Date.new(year, 5, 17) => "Grunnlovsdag",
72
72
  e + 49 => "1. pinsedag",
73
73
  e + 50 => "2. pinsedag",
@@ -87,44 +87,55 @@ def notable_dates(year)
87
87
  nov27 = Date.new(year, 11, 27)
88
88
  advent1 = nov27 + ((7 - nov27.cwday) % 7)
89
89
 
90
+ botsdag = last_wday(year, 10, 7) # last Sunday in October
91
+
90
92
  [
91
- [Date.new(year, 1, 1), "Nyttårsdag", true],
93
+ [Date.new(year, 1, 1), "Første nyttårsdag", true],
92
94
  [Date.new(year, 1, 21), "Prinsesse Ingrid Alexandra, #{year - 2004} år", false],
93
95
  [Date.new(year, 2, 6), "Samefolkets dag", false],
94
96
  [morsdag, "Morsdag", false],
95
- [Date.new(year, 2, 14), "St. Valentines dag", false],
97
+ [Date.new(year, 2, 14), "Valentinsdag", false],
96
98
  [fastelavn, "Fastelavn", false],
97
99
  [Date.new(year, 2, 21), "Kong Harald, #{year - 1937} år", false],
100
+ [Date.new(year, 3, 8), "Kvinnedagen", false],
98
101
  [Date.new(year, 3, 20), "Vårjevndøgn", false],
99
102
  [sommer_start, "Sommertid start", false],
100
103
  [e - 7, "Palmesøndag", true],
101
104
  [e - 3, "Skjærtorsdag", true],
102
105
  [e - 2, "Langfredag", true],
106
+ [e - 1, "Påskeaften", false],
103
107
  [e, "1. påskedag", true],
104
108
  [e + 1, "2. påskedag", true],
105
109
  [Date.new(year, 5, 1), "Arbeidernes dag", true],
106
110
  [Date.new(year, 5, 8), "Frigjøringsdag 1945", false],
107
- [e + 39, "Kr. Himmelfartsdag", true],
111
+ [e + 39, "Kristi himmelfartsdag", true],
108
112
  [Date.new(year, 5, 17), "Grunnlovsdag 1814", true],
113
+ [e + 48, "Pinseaften", false],
109
114
  [e + 49, "1. pinsedag", true],
110
115
  [e + 50, "2. pinsedag", true],
111
116
  [Date.new(year, 6, 7), "Unionsoppløsning 1905", false],
112
117
  [Date.new(year, 6, 21), "Sommersolverv", false],
113
- [Date.new(year, 6, 23), "Jonsokaften", false],
118
+ [Date.new(year, 6, 23), "Sankthansaften", false],
114
119
  [Date.new(year, 7, 4), "Dronning Sonja, #{year - 1937} år", false],
115
120
  [Date.new(year, 7, 20), "Kronprins Haakon, #{year - 1973} år", false],
116
121
  [Date.new(year, 7, 29), "Olsokdagen", false],
117
122
  [Date.new(year, 8, 19), "Kronprinsesse Mette-Marit, #{year - 1973} år", false],
118
123
  [Date.new(year, 9, 23), "Høstjevndøgn", false],
124
+ [botsdag, "Bots- og bededag", false],
119
125
  [sommer_slutt, "Sommertid slutt", false],
120
126
  [Date.new(year, 10, 31), "Halloween", false],
121
127
  [Date.new(year, 11, 1), "Allehelgensdag", false],
122
128
  [farsdag, "Farsdag", false],
123
129
  [advent1, "1. søndag i advent", false],
130
+ [advent1 + 7, "2. søndag i advent", false],
124
131
  [Date.new(year, 12, 13), "Luciadagen", false],
132
+ [advent1 + 14, "3. søndag i advent", false],
133
+ [advent1 + 21, "4. søndag i advent", false],
125
134
  [Date.new(year, 12, 21), "Vintersolverv", false],
135
+ [Date.new(year, 12, 24), "Julaften", false],
126
136
  [Date.new(year, 12, 25), "1. juledag", true],
127
137
  [Date.new(year, 12, 26), "2. juledag", true],
138
+ [Date.new(year, 12, 31), "Nyttårsaften", false],
128
139
  ].sort_by { |d, _, _| d }
129
140
  end
130
141
 
@@ -149,6 +160,7 @@ THEME = {
149
160
  $dark = ARGV.delete('--light') ? false : true
150
161
  $year = (ARGV[0] || Date.today.year).to_i
151
162
  $zoom = 0
163
+ $red_only = false
152
164
 
153
165
  def theme
154
166
  THEME[$dark ? :dark : :light]
@@ -170,56 +182,87 @@ def make_fonts
170
182
  }
171
183
  end
172
184
 
173
- root = TkRoot.new do
185
+ $root = TkRoot.new do
174
186
  title "norcal"
175
187
  end
176
188
 
177
- # -- Calendar content ----------------------------------------------------------
189
+ # -- Widget tracking for in-place updates --------------------------------------
190
+
191
+ # All trackable widgets, grouped by update type.
192
+ # Labels store {w: widget, fg: theme_key, bg: theme_key, font: font_key}.
193
+ # Buttons store {w: widget, fg: theme_key, text_key: theme_key or nil, text: static}.
194
+ $w = { bg: [], border: [], sep: [], labels: [], texts: [], buttons: [] }
195
+ $wn = { bg: [], border: [], sep: [], texts: [] } # notable-only (rebuilt on filter)
178
196
 
179
- def render_month(parent, year, month, holidays, f)
197
+ # Apply current theme + zoom to all tracked widgets (no widget recreation).
198
+ def restyle
180
199
  t = theme
200
+ f = make_fonts
201
+
202
+ $root.configure(background: t[:bg])
203
+ $canvas.configure(background: t[:bg])
204
+ $scrollbar.configure(background: t[:bg], troughcolor: t[:bg])
205
+ $root.title = "Kalender #{$year}"
206
+
207
+ ($w[:bg] + $wn[:bg]).each { |w| w.configure(background: t[:bg]) }
208
+ ($w[:border] + $wn[:border]).each { |w| w.configure(background: t[:bg], highlightbackground: t[:border]) }
209
+ ($w[:sep] + $wn[:sep]).each { |w| w.configure(background: t[:sep]) }
210
+
211
+ $w[:labels].each do |e|
212
+ e[:w].configure(foreground: t[e[:fg]], background: t[e[:bg]], font: f[e[:font]])
213
+ end
181
214
 
215
+ ($w[:texts] + $wn[:texts]).each do |tw|
216
+ tab = [65 + $zoom * 4, 40].max
217
+ tw.configure(background: t[:bg], font: f[:note], tabs: tab.to_s)
218
+ tw.tag_configure('dt', foreground: t[:fg], font: f[:note_b])
219
+ tw.tag_configure('dtr', foreground: t[:red], font: f[:note_b])
220
+ tw.tag_configure('desc', foreground: t[:desc_fg], font: f[:note_d])
221
+ end
222
+
223
+ $w[:buttons].each do |e|
224
+ text = e[:text_key] ? t[e[:text_key]] : (e[:text].respond_to?(:call) ? e[:text].call : e[:text])
225
+ e[:w].configure(text: text, font: f[:btn], foreground: t[e[:fg]],
226
+ background: t[:bg], activebackground: t[:bg], activeforeground: t[:fg])
227
+ end
228
+ end
229
+
230
+ # -- Build calendar (one-time per year) ----------------------------------------
231
+
232
+ def build_month(parent, year, month, holidays)
182
233
  first = Date.new(year, month, 1)
183
234
  last_day = Date.new(year, month, -1)
184
235
  monday = first - (first.cwday - 1)
185
236
  today = Date.today
186
237
 
187
- outer = TkFrame.new(parent) {
188
- borderwidth 1; relief 'solid'; background t[:bg]
189
- highlightbackground t[:border]; highlightthickness 0
190
- }
238
+ outer = TkFrame.new(parent) { borderwidth 1; relief 'solid'; highlightthickness 0 }
239
+ $w[:border] << outer
191
240
 
192
- grid = TkFrame.new(outer) { background t[:bg]; padx 6; pady 4 }
241
+ grid = TkFrame.new(outer) { padx 6; pady 4 }
242
+ $w[:bg] << grid
193
243
  grid.pack
194
244
 
195
- # Month name header
196
- TkLabel.new(grid) {
197
- text MONTHS_NO[month - 1]; font f[:mheader]; foreground t[:fg]; background t[:bg]
198
- }.grid(row: 0, column: 0, columnspan: 8, pady: [0, 2])
245
+ hdr = TkLabel.new(grid) { text MONTHS_NO[month - 1] }
246
+ $w[:labels] << { w: hdr, fg: :fg, bg: :bg, font: :mheader }
247
+ hdr.grid(row: 0, column: 0, columnspan: 8, pady: [0, 2])
199
248
 
200
- # Day-name headers
201
- TkLabel.new(grid) {
202
- text 'Uke'; font f[:cal]; foreground t[:dkgray]; background t[:bg]
203
- }.grid(row: 1, column: 0, sticky: 'e', padx: [0, 4])
249
+ uke = TkLabel.new(grid) { text 'Uke' }
250
+ $w[:labels] << { w: uke, fg: :dkgray, bg: :bg, font: :cal }
251
+ uke.grid(row: 1, column: 0, sticky: 'e', padx: [0, 4])
204
252
 
205
253
  DAYS_NO.each_with_index do |d, i|
206
- fg = case i
207
- when 5 then t[:gray]
208
- when 6 then t[:red]
209
- else t[:fg]
210
- end
211
- TkLabel.new(grid) {
212
- text d; font f[:cal_bold]; foreground fg; background t[:bg]
213
- }.grid(row: 1, column: i + 1, sticky: 'e')
254
+ fg_key = case i when 5 then :gray when 6 then :red else :fg end
255
+ lbl = TkLabel.new(grid) { text d }
256
+ $w[:labels] << { w: lbl, fg: fg_key, bg: :bg, font: :cal_bold }
257
+ lbl.grid(row: 1, column: i + 1, sticky: 'e')
214
258
  end
215
259
 
216
- # Week rows
217
260
  row = 2
218
261
  cur = monday
219
262
  while cur <= last_day
220
- TkLabel.new(grid) {
221
- text cur.cweek.to_s; font f[:cal]; foreground t[:dkgray]; background t[:bg]
222
- }.grid(row: row, column: 0, sticky: 'e', padx: [0, 4])
263
+ wk = TkLabel.new(grid) { text cur.cweek.to_s }
264
+ $w[:labels] << { w: wk, fg: :dkgray, bg: :bg, font: :cal }
265
+ wk.grid(row: row, column: 0, sticky: 'e', padx: [0, 4])
223
266
 
224
267
  7.times do |i|
225
268
  day = cur + i
@@ -227,263 +270,229 @@ def render_month(parent, year, month, holidays, f)
227
270
  is_red = day.cwday == 7 || holidays.key?(day)
228
271
  is_sat = day.cwday == 6 && !holidays.key?(day)
229
272
  is_today = day == today
230
- fg = if is_red then t[:red]
231
- elsif is_sat then t[:gray]
232
- else t[:fg]
233
- end
234
- TkLabel.new(grid) {
235
- text day.day.to_s
236
- font(is_today ? f[:cal_bold] : f[:cal])
237
- foreground fg
238
- background(is_today ? t[:today_bg] : t[:bg])
239
- }.grid(row: row, column: i + 1, sticky: 'e')
273
+ fg_key = is_red ? :red : (is_sat ? :gray : :fg)
274
+ bg_key = is_today ? :today_bg : :bg
275
+ font_key = is_today ? :cal_bold : :cal
276
+
277
+ lbl = TkLabel.new(grid) { text day.day.to_s }
278
+ $w[:labels] << { w: lbl, fg: fg_key, bg: bg_key, font: font_key }
279
+ lbl.grid(row: row, column: i + 1, sticky: 'e')
240
280
  end
241
281
  end
242
282
  row += 1
243
283
  cur += 7
244
284
  end
245
285
 
246
- # Pad empty rows so all months have the same height (max 6 week rows)
247
286
  while row < 8
248
- TkLabel.new(grid) { text ' '; font f[:cal]; background t[:bg] }
249
- .grid(row: row, column: 0)
287
+ pad = TkLabel.new(grid) { text ' ' }
288
+ $w[:labels] << { w: pad, fg: :fg, bg: :bg, font: :cal }
289
+ pad.grid(row: row, column: 0)
250
290
  row += 1
251
291
  end
252
292
 
253
- # Uniform column widths
254
293
  (1..7).each { |c| grid.grid_columnconfigure(c, uniform: 'day', minsize: 22) }
255
-
256
294
  outer
257
295
  end
258
296
 
259
- def render_notable(parent, year, f)
260
- t = theme
261
- dates = notable_dates(year)
297
+ def build_notable(parent, year)
298
+ $wn.each_value(&:clear)
262
299
 
263
- # Split into 3 roughly-equal columns
264
- third = (dates.size / 3.0).ceil
265
- cols = dates.each_slice(third).to_a
300
+ dates = notable_dates(year)
301
+ dates = dates.select { |_, _, is_red| is_red } if $red_only
302
+ third = [(dates.size / 3.0).ceil, 1].max
303
+ cols = dates.each_slice(third).to_a
266
304
 
267
- outer = TkFrame.new(parent) {
268
- borderwidth 1; relief 'solid'; background t[:bg]
269
- highlightbackground t[:border]; highlightthickness 0
270
- }
305
+ outer = TkFrame.new(parent) { borderwidth 1; relief 'solid'; highlightthickness 0 }
306
+ $wn[:border] << outer
271
307
 
272
- inner = TkFrame.new(outer) { background t[:bg] }
308
+ inner = TkFrame.new(outer)
309
+ $wn[:bg] << inner
273
310
  inner.pack(padx: 5, pady: 5, fill: 'both')
274
311
 
275
312
  cols.each_with_index do |entries, ci|
276
313
  tw = TkText.new(inner) {
277
314
  width 1; height(entries.size + 4)
278
- font f[:note]; borderwidth 0; highlightthickness 0
279
- wrap 'word'; background t[:bg]; padx 3; pady 2
280
- cursor ''; insertwidth 0
281
- tabs '65'
315
+ borderwidth 0; highlightthickness 0
316
+ wrap 'word'; padx 3; pady 2; cursor ''; insertwidth 0
282
317
  }
283
- tw.tag_configure('dt', foreground: t[:fg], font: f[:note_b])
284
- tw.tag_configure('dtr', foreground: t[:red], font: f[:note_b])
285
- tw.tag_configure('desc', foreground: t[:desc_fg], font: f[:note_d])
318
+ $wn[:texts] << tw
286
319
 
287
320
  entries.each_with_index do |(date, desc, is_red), i|
288
321
  dtag = is_red ? 'dtr' : 'dt'
289
- date_str = "#{date.day}. #{MABBR_NO[date.month - 1]}"
290
- tw.insert('end', date_str, dtag)
322
+ tw.insert('end', "#{date.day}. #{MABBR_NO[date.month - 1]}", dtag)
291
323
  tw.insert('end', "\t")
292
324
  tw.insert('end', desc, 'desc')
293
325
  tw.insert('end', "\n") unless i == entries.size - 1
294
326
  end
295
-
296
327
  tw.state 'disabled'
297
328
  tw.grid(row: 0, column: ci * 2, sticky: 'nsew', padx: [0, 2])
298
329
  inner.grid_columnconfigure(ci * 2, weight: 1, uniform: 'notecol')
299
330
  inner.grid_rowconfigure(0, weight: 1)
300
331
 
301
- # vertical separator between columns (except after last)
302
332
  if ci < cols.size - 1
303
- sep = TkFrame.new(inner) { background t[:sep]; width 1 }
333
+ sep = TkFrame.new(inner) { width 1 }
334
+ $wn[:sep] << sep
304
335
  sep.grid(row: 0, column: ci * 2 + 1, sticky: 'ns', padx: 4)
305
336
  end
306
337
  end
307
-
308
338
  outer
309
339
  end
310
340
 
311
- def build_calendar(parent, year, f)
312
- t = theme
341
+ def rebuild_notable
342
+ $notable_frame&.destroy
343
+ $notable_frame = build_notable($notable_parent, $year)
344
+ $notable_frame.grid(row: 4, column: 0, columnspan: 3, padx: 3, pady: [4, 0], sticky: 'ew')
345
+ restyle
346
+ end
347
+
348
+ def build_buttons(parent)
349
+ btn_bar = TkFrame.new(parent)
350
+ $w[:bg] << btn_bar
351
+ btn_bar.grid(row: 5, column: 0, columnspan: 3, pady: [4, 0])
352
+
353
+ b1 = TkButton.new(btn_bar) { relief 'flat'; borderwidth 0; command { $dark = !$dark; restyle } }
354
+ $w[:buttons] << { w: b1, fg: :dkgray, text_key: :toggle_label, text: nil }
355
+ b1.pack(side: 'left', padx: 4)
356
+
357
+ b2 = TkButton.new(btn_bar) { relief 'flat'; borderwidth 0; command { $zoom += 1; restyle } }
358
+ $w[:buttons] << { w: b2, fg: :dkgray, text_key: nil, text: '+' }
359
+ b2.pack(side: 'left', padx: 2)
360
+
361
+ b3 = TkButton.new(btn_bar) { relief 'flat'; borderwidth 0; command { $zoom -= 1 if $zoom > -8; restyle } }
362
+ $w[:buttons] << { w: b3, fg: :dkgray, text_key: nil, text: "\u2013" }
363
+ b3.pack(side: 'left', padx: 2)
364
+
365
+ b4 = TkButton.new(btn_bar) { relief 'flat'; borderwidth 0; command { $fit.call(false) } }
366
+ $w[:buttons] << { w: b4, fg: :dkgray, text_key: nil, text: 'fit' }
367
+ b4.pack(side: 'left', padx: 2)
368
+
369
+ b5 = TkButton.new(btn_bar) { relief 'flat'; borderwidth 0
370
+ command {
371
+ $red_only = !$red_only
372
+ rebuild_notable
373
+ restyle # update button text
374
+ }
375
+ }
376
+ $w[:buttons] << { w: b5, fg: :dkgray, text_key: nil,
377
+ text: -> { $red_only ? 'alle dager' : "bare r\u00f8de dager" } }
378
+ b5.pack(side: 'left', padx: 2)
379
+ end
380
+
381
+ def build_all(year)
382
+ $w.each_value(&:clear)
383
+ $inner_frame&.destroy
384
+
313
385
  holidays = red_days(year)
314
386
 
315
- frame = TkFrame.new(parent) { background t[:bg] }
387
+ $inner_frame = TkFrame.new($canvas)
388
+ $w[:bg] << $inner_frame
316
389
 
317
- # Single grid for months and notable dates
318
- grid_f = TkFrame.new(frame) { background t[:bg] }
319
- grid_f.pack(padx: 10, pady: [2, 8])
390
+ title = TkLabel.new($inner_frame) { text year.to_s }
391
+ $w[:labels] << { w: title, fg: :title_fg, bg: :bg, font: :title }
392
+ title.pack(pady: [8, 2])
393
+
394
+ frame = TkFrame.new($inner_frame)
395
+ $w[:bg] << frame
396
+ frame.pack(padx: 10, pady: [2, 8])
397
+
398
+ grid_f = TkFrame.new(frame)
399
+ $w[:bg] << grid_f
400
+ grid_f.pack
320
401
 
321
402
  12.times do |idx|
322
- m_frame = render_month(grid_f, year, idx + 1, holidays, f)
323
- m_frame.grid(row: idx / 3, column: idx % 3, padx: 3, pady: 3, sticky: 'nsew')
403
+ build_month(grid_f, year, idx + 1, holidays)
404
+ .grid(row: idx / 3, column: idx % 3, padx: 3, pady: 3, sticky: 'nsew')
324
405
  end
325
406
 
326
- # Notable dates in row 4, spanning all 3 month columns
327
- note_f = render_notable(grid_f, year, f)
328
- note_f.grid(row: 4, column: 0, columnspan: 3, padx: 3, pady: [4, 0], sticky: 'ew')
407
+ $notable_parent = grid_f
408
+ $notable_frame = build_notable(grid_f, year)
409
+ $notable_frame.grid(row: 4, column: 0, columnspan: 3, padx: 3, pady: [4, 0], sticky: 'ew')
329
410
 
330
- # Button bar: toggle + zoom
331
- btn_bar = TkFrame.new(grid_f) { background t[:bg] }
332
- btn_bar.grid(row: 5, column: 0, columnspan: 3, pady: [4, 0])
411
+ build_buttons(grid_f)
412
+ 3.times { |c| grid_f.grid_columnconfigure(c, weight: 1) }
333
413
 
334
- TkButton.new(btn_bar) {
335
- text t[:toggle_label]; font f[:btn]
336
- relief 'flat'; borderwidth 0
337
- foreground t[:dkgray]; background t[:bg]
338
- activebackground t[:bg]; activeforeground t[:fg]
339
- command { $dark = !$dark; $rebuild&.call }
340
- }.pack(side: 'left', padx: 4)
341
-
342
- TkButton.new(btn_bar) {
343
- text '+'; font f[:btn]
344
- relief 'flat'; borderwidth 0
345
- foreground t[:dkgray]; background t[:bg]
346
- activebackground t[:bg]; activeforeground t[:fg]
347
- command { $zoom += 1; $rebuild&.call }
348
- }.pack(side: 'left', padx: 2)
349
-
350
- TkButton.new(btn_bar) {
351
- text "\u2013"; font f[:btn] # en-dash for minus
352
- relief 'flat'; borderwidth 0
353
- foreground t[:dkgray]; background t[:bg]
354
- activebackground t[:bg]; activeforeground t[:fg]
355
- command { $zoom -= 1 if $zoom > -8; $rebuild&.call }
356
- }.pack(side: 'left', padx: 2)
357
-
358
- TkButton.new(btn_bar) {
359
- text 'fit'; font f[:btn]
360
- relief 'flat'; borderwidth 0
361
- foreground t[:dkgray]; background t[:bg]
362
- activebackground t[:bg]; activeforeground t[:fg]
363
- command { $fit&.call }
364
- }.pack(side: 'left', padx: 2)
414
+ # Embed in canvas
415
+ $canvas.delete('all')
416
+ $canvas_win = $canvas.create(:window, 0, 0, window: $inner_frame, anchor: 'nw')
365
417
 
366
- 3.times { |c| grid_f.grid_columnconfigure(c, weight: 1) }
418
+ center = proc {
419
+ cw = $canvas.winfo_width; ch = $canvas.winfo_height
420
+ fw = $inner_frame.winfo_reqwidth; fh = $inner_frame.winfo_reqheight
421
+ $canvas.coords($canvas_win, [(cw - fw) / 2, 0].max, [(ch - fh) / 2, 0].max)
422
+ $canvas.configure(scrollregion: "0 0 #{[cw, fw].max} #{[ch, fh].max}")
423
+ }
424
+ $inner_frame.bind('Configure', center)
425
+ $canvas.bind('Configure', center)
367
426
 
368
- frame
427
+ restyle
369
428
  end
370
429
 
371
430
  # -- Scrollable wrapper --------------------------------------------------------
372
431
 
373
- $scrollbar = TkScrollbar.new(root)
374
- $canvas = TkCanvas.new(root) {
375
- highlightthickness 0; borderwidth 0
376
- }
432
+ $scrollbar = TkScrollbar.new($root)
433
+ $canvas = TkCanvas.new($root) { highlightthickness 0; borderwidth 0 }
377
434
  $canvas.configure(yscrollcommand: proc { |*a| $scrollbar.set(*a) })
378
435
  $scrollbar.command(proc { |*a| $canvas.yview(*a) })
379
-
380
436
  $scrollbar.pack(side: 'right', fill: 'y')
381
437
  $canvas.pack(side: 'left', fill: 'both', expand: true)
382
438
 
383
- # Mouse wheel scrolling
384
- root.bind('MouseWheel') { |e| $canvas.yview_scroll(-e.delta / 120, 'units') }
385
- root.bind('Button-4') { $canvas.yview_scroll(-3, 'units') }
386
- root.bind('Button-5') { $canvas.yview_scroll(3, 'units') }
439
+ # Scrolling
440
+ $root.bind('MouseWheel') { |e| $canvas.yview_scroll(-e.delta / 120, 'units') }
441
+ $root.bind('Button-4') { $canvas.yview_scroll(-3, 'units') }
442
+ $root.bind('Button-5') { $canvas.yview_scroll(3, 'units') }
387
443
 
388
- # Keyboard zoom: Ctrl++ and Ctrl+-
389
- root.bind('Control-plus') { $zoom += 1; $rebuild&.call }
390
- root.bind('Control-equal') { $zoom += 1; $rebuild&.call } # Ctrl+= (unshifted +)
391
- root.bind('Control-minus') { $zoom -= 1 if $zoom > -8; $rebuild&.call }
444
+ # Keyboard zoom
445
+ $root.bind('Control-plus') { $zoom += 1; restyle }
446
+ $root.bind('Control-equal') { $zoom += 1; restyle }
447
+ $root.bind('Control-minus') { $zoom -= 1 if $zoom > -8; restyle }
392
448
 
393
449
  # Ctrl+mouse wheel zoom
394
- root.bind('Control-MouseWheel') { |e|
450
+ $root.bind('Control-MouseWheel') { |e|
395
451
  if e.delta > 0 then $zoom += 1 else $zoom -= 1 if $zoom > -8 end
396
- $rebuild&.call
452
+ restyle
397
453
  }
398
- root.bind('Control-Button-4') { $zoom += 1; $rebuild&.call }
399
- root.bind('Control-Button-5') { $zoom -= 1 if $zoom > -8; $rebuild&.call }
400
-
401
- # -- Build & rebuild -----------------------------------------------------------
454
+ $root.bind('Control-Button-4') { $zoom += 1; restyle }
455
+ $root.bind('Control-Button-5') { $zoom -= 1 if $zoom > -8; restyle }
402
456
 
403
- $inner_frame = nil
404
- $rebuild = nil
457
+ # -- Fit to window -------------------------------------------------------------
405
458
 
406
- $rebuild = proc {
407
- t = theme
408
- f = make_fonts
409
-
410
- $inner_frame&.destroy
411
- root.configure(background: t[:bg])
412
- $canvas.configure(background: t[:bg])
413
- $scrollbar.configure(background: t[:bg], troughcolor: t[:bg])
414
- root.title = "norcal - #{$year}"
415
-
416
- $inner_frame = TkFrame.new($canvas) { background t[:bg] }
417
-
418
- # Title
419
- TkLabel.new($inner_frame) {
420
- text $year.to_s; font f[:title]; foreground t[:title_fg]; background t[:bg]
421
- }.pack(pady: [8, 2])
422
-
423
- # Calendar content
424
- build_calendar($inner_frame, $year, f).pack(fill: 'both', expand: true)
425
-
426
- # Embed frame in canvas, centered
427
- $canvas.delete('all')
428
- $canvas_win = $canvas.create(:window, 0, 0, window: $inner_frame, anchor: 'nw')
429
-
430
- # Center content and update scroll region on resize
431
- center_content = proc {
432
- cw = $canvas.winfo_width
433
- ch = $canvas.winfo_height
434
- fw = $inner_frame.winfo_reqwidth
435
- fh = $inner_frame.winfo_reqheight
436
- x = [(cw - fw) / 2, 0].max
437
- y = [(ch - fh) / 2, 0].max
438
- $canvas.coords($canvas_win, x, y)
439
- $canvas.configure(scrollregion: "#{0} #{0} #{[cw, fw].max} #{[ch, fh].max}")
440
- }
441
-
442
- $inner_frame.bind('Configure', center_content)
443
- $canvas.bind('Configure', center_content)
444
- }
445
-
446
- # Fit-to-window: zoom so content fills available height, then resize window
447
- $fit = proc {
448
- # Measure current content height and extrapolate target zoom
459
+ $fit = proc { |resize_window|
460
+ target_h = resize_window ? $root.winfo_screenheight - 80 : $root.winfo_height
449
461
  cur_h = $inner_frame.winfo_reqheight.to_f
450
- screen_h = root.winfo_screenheight - 80
451
- base_avg = 13.0 # average base font size at zoom 0
462
+ base_avg = 13.0
452
463
 
453
- if cur_h > 0 && screen_h > 0
454
- # Current effective font size, and what it needs to be
455
- cur_font = base_avg + $zoom
456
- target_font = cur_font * (screen_h / cur_h)
464
+ if cur_h > 0 && target_h > 0
465
+ cur_font = base_avg + $zoom
466
+ target_font = cur_font * (target_h / cur_h)
457
467
  $zoom = (target_font - base_avg).floor
458
468
  $zoom = [$zoom, -8].max
459
469
  end
460
470
 
461
- $rebuild.call
471
+ restyle
462
472
  Tk.update_idletasks
463
473
 
464
- # If overshot, back off by one
465
- if $inner_frame.winfo_reqheight > screen_h
474
+ if $inner_frame.winfo_reqheight > target_h
466
475
  $zoom -= 1
467
- $rebuild.call
476
+ restyle
468
477
  Tk.update_idletasks
469
478
  end
470
479
 
471
- # Size window to actual content, capped at screen
472
- content_w = $inner_frame.winfo_reqwidth + 20
473
- content_h = $inner_frame.winfo_reqheight
474
- screen_w = root.winfo_screenwidth
475
- max_h = root.winfo_screenheight - 50
476
- w = [content_w, screen_w].min
477
- h = [content_h, max_h].min
478
- x = (screen_w - w) / 2
479
- root.geometry("#{w}x#{h}+#{x}+0")
480
+ if resize_window
481
+ cw = $inner_frame.winfo_reqwidth + 20
482
+ ch = $inner_frame.winfo_reqheight
483
+ sw = $root.winfo_screenwidth
484
+ mh = $root.winfo_screenheight - 50
485
+ w = [cw, sw].min; h = [ch, mh].min
486
+ $root.geometry("#{w}x#{h}+#{(sw - w) / 2}+0")
487
+ end
480
488
  }
481
489
 
482
- # Initial build, then fit to window (withdraw during startup to avoid flicker)
483
- root.withdraw
484
- $rebuild.call
490
+ # -- Startup -------------------------------------------------------------------
491
+
492
+ $root.withdraw
493
+ build_all($year)
485
494
  Tk.update_idletasks
486
- $fit.call
487
- root.deiconify
495
+ $fit.call(true)
496
+ $root.deiconify
488
497
 
489
498
  Tk.mainloop
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: norcal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - baosen