roxybooks

Check-in [aac465adc0]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:First commit.
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:aac465adc04bf9e7681afcab97f50b69bbe72f00
User & Date: cfkoch 2012-01-04 18:53:51
Context
2012-01-04
19:55
Added the license. Leaf check-in: 4320b59a04 user: cfkoch tags: trunk, v2.0.0
18:53
First commit. check-in: aac465adc0 user: cfkoch tags: trunk
18:50
initial empty check-in check-in: a2ca4ecf6a user: cfkoch tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added NOTES.























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Notes on the implementation of Roxybooks

## Christian Koch

Roxybooks is essentially a graphical front-end to an SQLite database written
with Tcl/Tk.


## Requirements

Tcl 8.5 and Tk 8.5 are required to run Roxybooks because the application
requires ttk::treeview, which is not available with earlier versions.
Furthermore, a reasonably recent shared library of SQLite compiled with Tcl
support is required. The version which ships with OS X 10.4 is too old; SQLite
needs to support the replace() function.

The final user of Roxybooks is, of course, Roxana, and she runs OS X 10.5, which
ships with Tcl/Tk 8.4. Thus, it is necessary to provide a pre-compiled binary of
tclsh8.5 and wish8.5. A simpler alternative is to ship ActiveState's
distribution of Tcl/Tk 8.5, but I want to cut down on as much overhead as
possible.


## Details

The database contains a single table of books defined as follows:

    CREATE TABLE books(
      id     INTEGER PRIMARY KEY,
      title  TEXT,
      author TEXT,
      genre  TEXT,
      year   INTEGER
    );

In terms of widgets, the table of books is implemented as a ttk::treeview, which
is not the optimal choice. This decision was made because solely there is no
veritable alternative in Tk's core library. Various extensions to Tk which
provide a traditional table do exist, but I opt not to install them for the sake
of simplicity.

ttk::treeview consists of a heirarchy of "items," each with a common set of
attributes, or columns. The id or name of each item is the id of the book as
defined in the database.

Books are sorted by any given column in a case-insensitive manner. The initial
word "The" in book titles is ignored.


## Should put in:

* Ignore the initial word "A" in book titles; e.g., "A Clockwork Orange" is
  listed under "C". 


## Nice to have:

* Sort authors by last name.
* Protect INSERT commands from statements with strange quote marks.

Added options.tcl.





























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
# options.tcl
# This file is a part of Roxybooks.
#
# Christian Koch <cfkoch@sdf.lonestar.org>
#

set TITLE   Roxybooks
set VERSION 2.0.0

set SQLITE_PATH "/usr/local/lib/sqlite3.7.9/libsqlite3.7.9.so"
set DB_PATH [file join $LOAD_PATH roxybooks.sqlite3] 

set WIDTH  800
set HEIGHT 600

set BOOK_DIALOG_WIDTH  300
set BOOK_DIALOG_HEIGHT 200

set COLUMN_ORDER     [list title author genre year]
set NUM_VISIBLE_ROWS 20

set ITEM_COLUMN_DEFAULT_WIDTH   10
set TITLE_COLUMN_DEFAULT_WIDTH  275
set AUTHOR_COLUMN_DEFAULT_WIDTH 150
set GENRE_COLUMN_DEFAULT_WIDTH  125
set YEAR_COLUMN_DEFAULT_WIDTH   55

set BANNER_BG black
set BANNER_FG deeppink

Added roxybooks.





































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#!/usr/bin/env wish8.5
#
# vim: filetype=tcl
#
# Roxybooks
# Christian Koch <cfkoch@sdf.lonestar.org>
#

set LOAD_PATH [file dirname [file normalize $argv0]]

source [file join $LOAD_PATH util.tcl]
source [file join $LOAD_PATH options.tcl]

load $SQLITE_PATH

#####

set column_ascending false
set last_column_clicked ""
array set given_book [list]

#####

# Prepare the database before we do anything else.
sqlite3 db $DB_PATH

catch {
  db eval {
    CREATE TABLE books(
      id     INTEGER PRIMARY KEY,
      title  TEXT,
      author TEXT,
      genre  TEXT,
      year   INTEGER
    );
  }
}

#####

# Window manager junk.
wm title . $TITLE
wm geometry . "${WIDTH}x${HEIGHT}"

ttk::frame .f1 -padding ""
ttk::frame .f2 -padding ""

#####

# A beautiful banner!
ttk::label .f1.banner -text $TITLE \
  -background $BANNER_BG -foreground $BANNER_FG \
  -font "helvetica 42"

#####

# Set up the tree view.
ttk::treeview .f1.tree \
  -columns $COLUMN_ORDER -displaycolumns "#all" \
  -height $NUM_VISIBLE_ROWS

for {set i 0} {$i < [llength $COLUMN_ORDER]} {incr i} {
  .f1.tree heading $i \
    -text [string totitle [lindex $COLUMN_ORDER $i]] \
    -command "arrange_by [lindex $COLUMN_ORDER $i]"
}

.f1.tree column #0     -width $ITEM_COLUMN_DEFAULT_WIDTH
.f1.tree column title  -width $TITLE_COLUMN_DEFAULT_WIDTH
.f1.tree column author -width $AUTHOR_COLUMN_DEFAULT_WIDTH
.f1.tree column genre  -width $GENRE_COLUMN_DEFAULT_WIDTH
.f1.tree column year   -width $YEAR_COLUMN_DEFAULT_WIDTH

ttk::scrollbar .f1.scroller -orient vertical -command {.f1.tree yview}
.f1.tree configure -yscrollcommand {.f1.scroller set}

#####

bind .f1.tree <KeyRelease-BackSpace> delete_selected_row

proc delete_selected_row {} {
  global db last_column_clicked given_book

  # Do nothing if nothing in the table is selected.
  if {[not [strtobool [.f1.tree selection]]]} {return}

  set this_book [.f1.tree set [.f1.tree selection]]
  set given_book(title)  [dict get $this_book title]
  set given_book(author) [dict get $this_book author]
  set given_book(year)   [dict get $this_book year]

  set response [
    tk_messageBox -title "Delete a book" -type okcancel -default cancel \
      -message "Delete this book?\n\n\"$given_book(title)\" ($given_book(year)) by $given_book(author)" \
  ]

  if {[string eq $response ok]} {
    db eval "DELETE FROM books WHERE id == [.f1.tree selection];"
    arrange_by $last_column_clicked false
  }
}

#####

# Remove all the entries in the tree, then repopulate it with all the books
# displayed in the requested order. If 'flip' is true (the default), then the
# value of $column_ascending is inverted.
proc arrange_by {column {flip true}} {
  global column_ascending last_column_clicked
  .f1.tree delete [.f1.tree children {}]

  if {$flip} {set column_ascending [not $column_ascending]}
  expr {$column_ascending ? [set ascstring "asc"] : [set ascstring "desc"]}

  db eval "
    SELECT * FROM books ORDER BY replace(lower($column), 'the ', '') $ascstring
  " book {
    .f1.tree insert {} end \
      -id $book(id) \
      -values [list $book(title) $book(author) $book(genre) $book(year)]
  }

  set last_column_clicked $column
}

#####

# "About" button
ttk::button .f2.about_button -text "About..." -command do_about_button

proc do_about_button {} {
  global TITLE VERSION

  tk_messageBox -type ok -title "About $TITLE" \
    -message "$TITLE v$VERSION\n\nWritten with Tcl/Tk by Christian Koch"
}

#####

# The "Add" button and the "Edit" button present the same dialog box, but in two
# different ways.
ttk::button .f2.add_button -text "Add" -command {show_book_dialog}
ttk::button .f2.edit_button -text "Edit" -command {show_book_dialog false}

# If 'adding' is true (the default), we insert a new book. Otherwise, we update
# an existing entry.
proc show_book_dialog {{adding true}} {
  global BOOK_DIALOG_WIDTH BOOK_DIALOG_HEIGHT given_book
  toplevel .book_dialog

  if {$adding} {
    set book_dialog_title "Add a book"
  } else {
    set book_dialog_title "Edit a book"
  }
  
  wm title .book_dialog "Add a book"
  wm geometry .book_dialog "${BOOK_DIALOG_WIDTH}x${BOOK_DIALOG_HEIGHT}"

  ttk::label .book_dialog.title_label  -text "Title"
  ttk::label .book_dialog.author_label -text "Author"
  ttk::label .book_dialog.genre_label  -text "Genre"
  ttk::label .book_dialog.year_label   -text "Year"

  # If we're inserting a new book, then 'given_book' must be unset. If we're
  # editing an existing book entry, then set 'given_book' appropriately.
  if {$adding} {
    array unset given_book
  } else {
    set this_book [.f1.tree set [.f1.tree selection]]
    set given_book(title)  [dict get $this_book title]
    set given_book(author) [dict get $this_book author]
    set given_book(genre)  [dict get $this_book genre]
    set given_book(year)   [dict get $this_book year]
  }

  ttk::entry .book_dialog.title_entry  -textvar given_book(title)
  ttk::entry .book_dialog.author_entry -textvar given_book(author)
  ttk::entry .book_dialog.genre_entry  -textvar given_book(genre)
  ttk::entry .book_dialog.year_entry   -textvar given_book(year)

  ttk::button .book_dialog.ok_button -text "OK"
  ttk::button .book_dialog.cancel_button -text "Cancel" \
    -command {destroy .book_dialog}

  expr {$adding ? [set ok_command insert_book] : [set ok_command update_book]}
  .book_dialog.ok_button configure -command $ok_command 
  
  # Everything's set. Now arrange it!
  grid .book_dialog.title_label   -row 0 -column 0
  grid .book_dialog.title_entry   -row 0 -column 1
  grid .book_dialog.author_label  -row 1 -column 0
  grid .book_dialog.author_entry  -row 1 -column 1
  grid .book_dialog.genre_label   -row 2 -column 0
  grid .book_dialog.genre_entry   -row 2 -column 1
  grid .book_dialog.year_label    -row 3 -column 0
  grid .book_dialog.year_entry    -row 3 -column 1
  grid .book_dialog.ok_button     -row 4 -column 0
  grid .book_dialog.cancel_button -row 4 -column 1
}

proc clean_up_book_dialog {} {
  global given_book last_column_clicked

  destroy .book_dialog
  array unset given_book
  arrange_by $last_column_clicked false
}

proc insert_book {} {
  global db given_book last_column_clicked

  db eval "
    INSERT INTO books VALUES(
      null, 
      \"$given_book(title)\",
      \"$given_book(author)\",
      \"$given_book(genre)\",
      $given_book(year)
    );
  "

  clean_up_book_dialog
}

proc update_book {} {
  global db given_book last_column_clicked

  db eval "
    UPDATE books SET
      title  = \"$given_book(title)\",
      author = \"$given_book(author)\",
      genre  = \"$given_book(genre)\",
      year   = $given_book(year)
    WHERE id == [.f1.tree selection];
  "

  clean_up_book_dialog
}

#####

# Arrange all the widgets.
grid .f1 -row 0 -column 0 -sticky nsew
grid .f2 -row 1 -column 0 -sticky nsew -pady 15

grid .f1.banner   -row 0 -column 0 -columnspan 2 -sticky ew
grid .f1.tree     -row 1 -column 0
grid .f1.scroller -row 1 -column 1 -sticky ns

grid .f2.about_button  -row 0 -column 0 -padx {0 10}
grid .f2.add_button    -row 0 -column 1 -padx {0 10}
grid .f2.edit_button   -row 0 -column 2 -padx {0 10}

#####

# All that for this:
arrange_by "title"

Added util.tcl.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#
# util.tcl
# This file is a part of Roxybooks.
#
# Christian Koch <cfkoch@sdf.lonestar.org>
#

# Return the logical NOT of boolean 'b'.
proc not {b} {
  if {$b} {return false} else {return true}
}

# If 's' is the empty string, return false. True otherwise.
proc strtobool {s} {
  if {[string equal $s ""]} {return false} else {return true}
}