Programming Gambas from Zip/Tray Icon Notebook

Design a Notebook Tray Icon

edit

The idea for this program is simple: whatever text you have on the clipboard, click on a handy little tray icon and it will be saved to an SQLite database. Nothing seems to happen, but the text is saved in a new record.

There is also the means to search for notes already saved. This is done in a window that appears when you middle-click the tray icon. I would have preferred it to appear on right-clicking the icon, but right-clicking can only make a menu appear. That is what right-clicks do: they show contextual menus. Now, middle-clicking is fine if you have a mouse with a middle wheel or button that can be clicked. For those of us who use a laptop’s trackpad, middle-clicking is simulated by clicking both left and right buttons simultaneously. On KDE there is an option in System Settings that can disable this, but by default it is enabled.

Experiment with the main window being hidden when the application starts if you like, or not the skiptaskbar property so the minimised main window does not get listed with the other open applications in the panel, but the effects did not appeal at all so they have been left at their defaults.

A Google image search will find a suitable picture for the tray icon:

 

It does not need to be any particular size. It scales itself nicely when the program runs.

The window has a large text area to show a note, and a textbox at the top left in which to type search text. There are also three labels that display the date/time when the note was saved, the position of the note among all the notes that have been found with that search text in them, and, for interest, the record ID of the note.

 

 

The tray icon (bottom left corner) can be placed anywhere. It does not appear in the window. It appears in the system tray (usually at the bottom right corner of the screen, in KDE’s default panel).

The tray icon comes in its own component, so check Project > Properties to see that it and the database components are included:

 

In summary, to use this notebook:

  1. Copy any text to the clipboard.
  2. Click the tray icon and your text is saved.
  3. Click the tray icon with both left and right mouse buttons at the same time to search for a note.

Sometimes it is useful to save the text then bring up the window and add some key words that will help you find the note again. I sometimes add the words JOKE or QUOTE or FAMILY HISTORY. That way, by typing “QUOTE” in the search box, all my quotes appear and I can step through them one at a time. Copying all selected notes would be a useful feature to add.

The SQL select statement appears in the window caption, for interest.

For good measure, a TEXT menu has four entries for adjusting text:

 

Type Extra… positions the cursor at the end of the text of the visible note, ready for you to type something extra.

Tidy gets rid of multiple spaces, multiple tabs and multiple blank lines, and removes leading and trailing spaces.

Sentences joins broken sentences. Text copied from emails often has distinct lines.

Double-space separates the paragraphs with a blank line.

These text operations work on the whole of the text if there is no selection, or on the selected text if there is a selection. The shortcuts are there because often I find myself doing the last three in quick succession to get the text looking decent.

The other menus are:

 

The database is an SQLite file. You might wonder where the OPEN and NEW menu items are. For simplicity, the program creates a new database when it opens if none exists with the name and in the place it expects to find it. It is called Notebook.sqlite and it is in the Home directory. It is easy enough to change the code to make it in the Documents directory if you wish.

File > Backup is a useful menuitem that copies the database file to a Backups folder in the Documents folder, date-stamping the file name to show when it was created and to not interfere with earlier backups.

File > Renumber makes the record ID’s sequential, with no gaps. It, and the Vacuum item below it, are unnecessary. Vacuum tidies the internal structure of the database file. It uses SQLite’s inbuilt vacuum command.

File > Quit closes the application. File > Hide makes the window invisible. Clicking the close box on the window also only hides the window; it does not close the application. For this trick, the window’s Persistent property is set to true. The window persists, invisibly, when its close box is clicked.

When you type in the search textbox an SQL Select statement selects all the notes that contain that text. It is a simple search: it does not search for notes containing any of the words, ordered from best to least matching. It searches for exactly the text that is typed. As you type each letter the search is performed. A public property in the mdb module, Public rs As Result, stores the results of the search. Notes > Show All clears the search and finds all the notes. This happens when the program starts: all notes are selected.

Notes > New… lets you type a new note that is saved when you leave the text area.

Notes > Delete deletes the note currently displayed from the database.

Notes > Clear All clears all notes from the database. There is no undo but there is a chance to bail.

Window properties are:

Arrangement = Vertical

Stacking = Above

Width = 800

Height = 500

The Tray Icon (ti1) properties that need to be set are:

Tooltip = Click to save clipboard text Visible = True

Be sure to set it to visible or you will not see the tray icon when the program is run, and there will be no elegant way of quitting the program when it is running after you hide the window that shows initially.

The Stacking property ensures that the window remains above other windows. If you click in the window belonging to your web browser, for example, the Notebook window does not get covered by the browser window but remains on top, floating above it. This can be useful for utility type programs.

The last thing to note before getting to the code is the use of the keyboard’s arrow keys. UP and DOWN take you to the first and last of the found notes respectively. LEFT and RIGHT step you back or forwards through the found notes. “Found notes” means those that are found to have the string of text in them that you typed in the textbox, or, if nothing has been typed, all the notes.

A few comments follow the code.

Code relating to the database is collected in a module called mdb.

mdb Module Code

edit
' Gambas module file

Public db1 As New Connection
Public rs As Result
Public SQLSel As String

Public Sub CreateDatabase()

  db1.Type = "sqlite"
  db1.host = User.home
  db1.name = ""

  'delete an existing Notebook.sqlite
  If Exist(User.home & "/Notebook.sqlite") Then
    Kill User.home & "/Notebook.sqlite"
  Endif

  'create Notebook.sqlite
  db1.Open
  db1.Databases.Add("Notebook.sqlite")
  db1.Close

End

Public Sub ConnectDatabase()

  db1.Type = "sqlite"
  db1.host = User.home
  db1.name = "Notebook.sqlite"
  db1.Open
  SelectAllNotes
  Debug("Notes file connected:<br>" & db1.Host &/ db1.Name & "<br><br>" & rs.Count & " records")

End

Public Sub MakeTable()

  Dim hTable As Table

  db1.name = "Notebook.sqlite"
  db1.Open
  hTable = db1.Tables.Add("Notes")
  hTable.Fields.Add("KeyID", db.Integer)
  hTable.Fields.Add("Created", db.Date)
  hTable.Fields.Add("Note", db.String)
  hTable.PrimaryKey = ["KeyID"]
  hTable.Update
  Message("Notes file created:<br>" & db1.Host &/ db1.Name)

End

Public Sub SelectAllNotes()

  rs = db1.Exec("SELECT * FROM Notes")
  FMain.Caption = "SELECT * FROM Notes"
  rs.MoveLast

End

Public Sub Massage(z As String) As String

  While InStr(z, "''") > 0 'this avoids a build-up of single apostrophes
    Replace(z, "''", "'")
  Wend
  Return Replace(z, "'", "''")

End

Public Sub AddRecord(s As String, t As Date) As String

  Dim rs1 As Result
  Dim NextID As Integer

  If rs.Max = -1 Then NextID = 1 Else NextID = db1.Exec("SELECT Max(KeyID) AS TheMax FROM Notes")!TheMax + 1

  db1.Begin
  rs1 = db1.Create("Notes")
  rs1!KeyID = NextID
  rs1!Created = t 'time
  rs1!Note = Massage(s)
  rs1.Update
  db1.Commit
  SelectAllNotes
  Return NextID

Catch
  db1.Rollback
  Message.Error(Error.Text)

End

Public Sub UpdateRecord(RecNum As Integer, NewText As String)

  db1.Exec("UPDATE Notes SET Note='" & Massage(NewText) & "' WHERE KeyID=" & RecNum)
  Dim pos As Integer = rs.Index
  'Refresh the result cursor, so the text in it is updated as well as in the database file. This is tricky.
  If IsNull(SQLSel) Then rs = db.Exec("SELECT * FROM Notes") Else rs = db.Exec(SQLSel) 'SQLSel is the last search, set by typing in tbSearch
  rs.MoveTo(pos) 'Ooooh yes! It did it.

Catch
  Message.Error("<b>Update error.</b><br><br>" & Error.Text)

End

Public Sub MoveRecord(KeyCode As Integer) As Boolean

  If rs.Count = 0 Then Return False
  Select Case KeyCode
    Case Key.Left
      If rs.Index > 0 Then rs.MovePrevious Else rs.MoveLast
      Return True
    Case Key.Right
      If rs.Index < rs.Max Then rs.MoveNext Else rs.MoveFirst
      Return True
    Case Key.Up
      rs.MoveFirst
      Return True
    Case Key.Down
      rs.MoveLast
      Return True
  End Select
  Return False

End

Public Sub ClearAll()

  If Message.Warning("Delete all notes? This cannot be undone.", "Ok", "Cancel") = 1 Then
    db1.Exec("DELETE FROM Notes")
    SelectAllNotes
  Endif

End

Public Sub DeleteRecord(RecNum As Integer)

  db1.Exec("DELETE FROM Notes WHERE KeyID='" & RecNum & "'")
  SelectAllNotes

End

Public Sub SearchFor(s As String)

  SQLSel = "SELECT * FROM Notes WHERE Note LIKE '%" & Massage(s) & "%'"

  If IsNull(s) Then
    SelectAllNotes
    SQLSel = ""
  Else
    FMain.Caption = SQLSel
    rs = db1.Exec(SQLSel)
  Endif

End

Public Sub Renumber()

  Dim res As Result = db.Exec("SELECT * FROM Notes ORDER BY KeyID")
  Dim i As Integer = 1
  Dim x As Integer

  Application.Busy += 1
  While res.Available
    x = res!KeyID
    db.Exec("UPDATE Notes SET KeyID=" & i & " WHERE KeyID=" & x)
    i += 1
    res.MoveNext
  Wend
  SelectAllNotes
  Application.Busy -= 1

End

Public Sub Vacuum() As String

  Dim fSize1, fSize2 As Float

  fSize1 = Stat(db1.Host &/ db1.Name).Size / 1000 'kB
  db1.Exec("Vacuum")
  fSize2 = Stat(db1.Host &/ db1.Name).Size / 1000 'kB
  Dim Units As String = "kB"
  If fSize1 > 1000 Then 'megabyte range
    fSize1 /= 1000
    fSize2 /= 1000
    Units = "MB"
  Endif
  Return Format(fSize1, "#.0") & Units & " -> " & Format(fSize2, "#.0") & Units & " (" & Format(fSize1 - fSize2, "#.00") & Units & ")"

End

Public Sub Backup() As String

  If Not Exist(User.Home &/ "Documents/Backups/") Then Mkdir User.Home &/ "Documents/Backups"
  Dim fn As String = "Notebook " & Format(Now, "yyyy-mm-dd hh-nn")
  Dim source As String = db1.Host &/ db1.Name
  Dim dest As String = User.Home &/ "Documents/Backups/" & fn
  Try Copy source To dest
  If Error Then Return "Couldn't save -> " & Error.Text Else Return "Saved ->  /Documents/Backups/" & fn

End

The main form’s code

edit
' Gambas class file

Public OriginalText As String

Public Sub ti1_Click()

  Dim TimeAdded As String

  If Clipboard.Type = Clipboard.Text Then TimeAdded = mdb.AddRecord(Clipboard.Paste("text/plain"), Now())

End

Public Sub Form_Open()

  If Not Exist(User.Home &/ "Notebook.sqlite") Then 'create notebook data file
    mdb.CreateDatabase
    mdb.MakeTable
    mdb.SelectAllNotes
  Else
    mdb.ConnectDatabase
    ShowRecord
  Endif

End

Public Sub MenuQuit_Click()

  mdb.db1.Close
  ti1.Delete
  Quit

End

Public Sub Form_KeyPress()

  If mdb.MoveRecord(Key.Code) Then
    ShowRecord
    Stop Event
  Endif

End

Public Sub ShowRecord()

  If mdb.rs.count = 0 Then
    ClearFields
    Return
  Endif
  ta1.Text = Replace(mdb.rs!Note, "''", "'")
  Dim d As Date = mdb.rs!Created
  labTime.text = Format(d, gb.MediumDate) & "  " & Format(d, gb.LongTime)
  labRecID.text = mdb.rs!KeyID
  labLocation.text = Str(mdb.rs.Index + 1) & "/" & mdb.rs.Count
  OriginalText = ta1.Text

End

Public Sub MenuClear_Click()

  mdb.ClearAll
  ClearFields
  labTime.Text = "No records"

End

Public Sub MenuCopy_Click()

  Clipboard.Copy(ta1.Text)

End

Public Sub MenuDeleteNote_Click()

  Dim RecNum As Integer = Val(labRecID.Text)

  mdb.DeleteRecord(RecNum) 'after which all records selected; now to relocate...
  Dim res As Result = db.Exec("SELECT * FROM Notes WHERE KeyID<" & RecNum)
  res.MoveLast
  Dim i As Integer = res.Index
  mdb.rs.MoveTo(i)
  ShowRecord

End

Public Sub ClearFields()

  ta1.Text = ""
  labRecID.Text = ""
  labTime.Text = ""
  labLocation.Text = ""

End

Public Sub MenuNewNote_Click()

  ClearFields
  ta1.SetFocus

End

Public Sub ta1_GotFocus()

  OriginalText = ta1.Text

End

Public Sub ta1_LostFocus()

  If ta1.Text = OriginalText Then Return 'no change
  SaveOrUpdate

End

Public Sub tbSearch_Change()

  mdb.SearchFor(tbSearch.Text)
  ShowRecord

Catch
  Message.Error(Error.Text)

End

Public Sub ta1_KeyPress()

  If Key.Code = Key.Esc Then Me.SetFocus 'clear focus from textarea; this triggers a record update

End

Public Sub tbSearch_KeyPress()

  If Key.Code = Key.Esc Then Me.SetFocus

End

Public Sub MenuShowAll_Click()

  mdb.SelectAllNotes
  ShowRecord

End

Public Sub KeepReplacing(InThis As String, LookFor As String, Becomes As String) As String

  Dim z As String = InThis

  While InStr(z, LookFor) > 0
    z = Replace(z, LookFor, Becomes)
  Wend
  Return z

End

Public Sub SaveOrUpdate()

  If IsNull(labRecID.Text) Then 'new record
    If IsNull(ta1.Text) Then Return
    Dim d As Date = Now()
    labRecID.Text = mdb.AddRecord(ta1.Text, d)
    labTime.text = Format(d, gb.MediumDate) & "  " & Format(d, gb.LongTime)
  Else 'update
    If IsNull(ta1.Text) Then
      mdb.DeleteRecord(Val(labRecID.Text))
      ClearFields 'maybe leave everything empty?
    Else
      mdb.UpdateRecord(Val(labRecID.Text), ta1.Text)
    Endif
  Endif

End

Public Sub MenuTidy_Click()

  OriginalText = ta1.Text
  If IsNull(ta1.Text) Then Return
  Dim z As String = If(ta1.Selection.Length = 0, Trim(ta1.Text), Trim(ta1.Selection.Text))
  z = KeepReplacing(z, gb.NewLine, "|")
  z = KeepReplacing(z, gb.Tab & gb.Tab, gb.Tab)
  z = KeepReplacing(z, "  ", " ")
  z = KeepReplacing(z, "| ", "|")
  z = KeepReplacing(z, "|" & gb.tab, "|")
  z = KeepReplacing(z, "||", "|")
  z = KeepReplacing(z, "|", gb.NewLine)
  If ta1.Selection.Length = 0 Then ta1.Text = z Else ta1.Selection.Text = z
  SaveOrUpdate

End

Public Sub MenuSentences_Click()

  OriginalText = ta1.Text
  If IsNull(ta1.Text) Then Return
  Dim z As String = If(ta1.Selection.Length = 0, Trim(ta1.Text), Trim(ta1.Selection.Text))
  z = KeepReplacing(z, gb.NewLine, "~")
  z = KeepReplacing(z, "~ ", "~")
  z = KeepReplacing(z, ".~", "|")
  z = KeepReplacing(z, "~", " ")
  z = KeepReplacing(z, "  ", " ")
  z = KeepReplacing(z, "|", "." & gb.NewLine)
  If ta1.Selection.Length = 0 Then ta1.Text = z Else ta1.Selection.Text = z
  SaveOrUpdate

End

Public Sub MenuUndo_Click()

  Dim z As String = ta1.Text

  ta1.Text = OriginalText
  OriginalText = z
  SaveOrUpdate

End

Public Sub MenuRenumber_Click()

  mdb.Renumber
  ShowRecord

End

Public Sub MenuVacuum_Click()

  Me.Caption = "File size -> " & mdb.Vacuum()

End

Public Sub MenuBackup_Click()

  Me.Caption = mdb.Backup()

End

Public Sub MenuTypeExtra_Click()

  ta1.SetFocus
  ta1.Text &= gb.NewLine & gb.NewLine
  ta1.Select(ta1.Text.Len)

End

Public Sub MenuDoubleSpace_Click()

  OriginalText = ta1.Text
  If IsNull(ta1.Text) Then Return
  Dim z As String = If(ta1.Selection.Length = 0, Trim(ta1.Text), Trim(ta1.Selection.Text))
  z = KeepReplacing(z, gb.NewLine, "|")
  z = KeepReplacing(z, "||", "|")
  z = KeepReplacing(z, "|", gb.NewLine & gb.NewLine)
  If ta1.Selection.Length = 0 Then ta1.Text = z Else ta1.Selection.Text = z
  SaveOrUpdate

End

Public Sub ti1_MiddleClick()

  Me.Show
  Me.Activate
  tbSearch.Text = ""
  mdb.SelectAllNotes
  ShowRecord

End

Public Sub MenuHide_Click()

  Me.Hide

End

Two useful functions

edit

The Massage(string) function is necessary for handling the saving of text that has single apostrophes in it. SQL statements use single apostrophes to surround strings. A single apostrophe in the string will terminate the string and what follows will be a syntax error as it will be incomprehensible. To include an apostrophe it has to be doubled. For example, to save the string Fred’s house it has to first be converted (“massaged”) to Fred’’s house.

The KeepReplacing(InThis, LookFor, ReplaceWithThis) function performs replacements until the LookFor string is no longer present. For example, if you wanted to remove multiple x’s from abcxxxxdef and just have one single x you cannot just use Replace(“abcxxxxdef”, “xx”, “x”), for this would produce abcxxdef. The first double-x becomes a single x, and the second double-x becomes a single x. You still have a double-x. You have to keep replacing until there are no more double-x’s.

Last Words

edit

That’s all, folks, except for the reference appendices. May I finish where I began, with a word of thanks to Benoît Minisini. This programming environment is a delight to use. With the gratitude of all of us users we sing, glass in hand, “For he’s a jolly good fellow, and so say all of us”.

Gerard Buzolic

25 September 2019

Programming Gambas from Zip
 ← Printing Tray Icon Notebook Afterword →