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.
- checksums.yaml +4 -4
- data/README.md +3 -2
- data/bin/norcal +217 -208
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfb181fb12eccf2f5a2a573755d884474dc74eebddf4391fd6a88c5adc55ec7d
|
|
4
|
+
data.tar.gz: a6fe09085766141d71509c87c8ff79aec265453c1456eac18646112a39fe599e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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) => "
|
|
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 => "
|
|
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), "
|
|
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), "
|
|
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, "
|
|
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), "
|
|
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
|
-
# --
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
241
|
+
grid = TkFrame.new(outer) { padx 6; pady 4 }
|
|
242
|
+
$w[:bg] << grid
|
|
193
243
|
grid.pack
|
|
194
244
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
TkLabel.new(grid) {
|
|
235
|
-
|
|
236
|
-
|
|
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 ' '
|
|
249
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
dates = notable_dates(year)
|
|
297
|
+
def build_notable(parent, year)
|
|
298
|
+
$wn.each_value(&:clear)
|
|
262
299
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
279
|
-
wrap 'word';
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
312
|
-
|
|
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
|
-
|
|
387
|
+
$inner_frame = TkFrame.new($canvas)
|
|
388
|
+
$w[:bg] << $inner_frame
|
|
316
389
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
389
|
-
root.bind('Control-plus') { $zoom += 1;
|
|
390
|
-
root.bind('Control-equal') { $zoom += 1;
|
|
391
|
-
root.bind('Control-minus') { $zoom -= 1 if $zoom > -8;
|
|
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
|
-
|
|
452
|
+
restyle
|
|
397
453
|
}
|
|
398
|
-
root.bind('Control-Button-4') { $zoom += 1;
|
|
399
|
-
root.bind('Control-Button-5') { $zoom -= 1 if $zoom > -8;
|
|
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
|
-
|
|
404
|
-
$rebuild = nil
|
|
457
|
+
# -- Fit to window -------------------------------------------------------------
|
|
405
458
|
|
|
406
|
-
$
|
|
407
|
-
|
|
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
|
-
|
|
451
|
-
base_avg = 13.0 # average base font size at zoom 0
|
|
462
|
+
base_avg = 13.0
|
|
452
463
|
|
|
453
|
-
if cur_h > 0 &&
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
471
|
+
restyle
|
|
462
472
|
Tk.update_idletasks
|
|
463
473
|
|
|
464
|
-
|
|
465
|
-
if $inner_frame.winfo_reqheight > screen_h
|
|
474
|
+
if $inner_frame.winfo_reqheight > target_h
|
|
466
475
|
$zoom -= 1
|
|
467
|
-
|
|
476
|
+
restyle
|
|
468
477
|
Tk.update_idletasks
|
|
469
478
|
end
|
|
470
479
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
#
|
|
483
|
-
|
|
484
|
-
$
|
|
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
|