Moved history to the (app)menu
[quicknote] / src / quicknote_gtk.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5  Copyright (C) 2007 Christoph W├╝rstle
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 @todo Add Note Export (txt File) and Export All (json dump?)
12 @todo Remove confirmation on deleting empty notes
13 @todo Try to switch to more passive notifications (rather than message boxes)
14 """
15
16 from __future__ import with_statement
17
18 import os
19 import gc
20 import logging
21 import warnings
22 import ConfigParser
23
24 import gtk
25
26 try:
27         import hildon
28 except ImportError:
29         import fakehildon as hildon
30
31 try:
32         import osso
33 except ImportError:
34         osso = None
35
36 import constants
37 import hildonize
38 import gtk_toolbox
39
40 import speichern
41 import kopfzeile
42 import notizen
43 import sync
44
45
46 try:
47         _
48 except NameError:
49         _ = lambda x: x
50
51
52 _moduleLogger = logging.getLogger("quicknote_gtk")
53
54
55 class QuicknoteProgram(hildonize.get_app_class()):
56
57         _user_data = os.path.join(os.path.expanduser("~"), ".%s" % constants.__app_name__)
58         _user_settings = "%s/settings.ini" % _user_data
59
60         def __init__(self):
61                 super(QuicknoteProgram, self).__init__()
62                 self._clipboard = gtk.clipboard_get()
63
64                 dblog = os.path.join(self._user_data, "quicknote.log")
65
66                 _moduleLogger.info('Starting quicknote')
67                 self._wordWrapEnabled = False
68
69                 self._window_in_fullscreen = False #The window isn't in full screen mode initially.
70                 self._isZoomEnabled = False
71
72                 self._db = speichern.Speichern()
73                 self._syncDialog = None
74
75                 #Create GUI main vbox
76                 vbox = gtk.VBox(homogeneous = False, spacing = 0)
77
78                 if hildonize.GTK_MENU_USED:
79                         #Create Menu and apply it for hildon
80                         filemenu = gtk.Menu()
81
82                         menu_items = gtk.MenuItem(_("Set DB file"))
83                         filemenu.append(menu_items)
84                         menu_items.connect("activate", self.set_db_file, None)
85
86                         menu_items = gtk.MenuItem(_("SQL History"))
87                         filemenu.append(menu_items)
88                         menu_items.connect("activate", self._on_view_sql_history, None)
89
90                         menu_items = gtk.MenuItem(_("Sync notes"))
91                         filemenu.append(menu_items)
92                         menu_items.connect("activate", self._on_sync_notes, None)
93
94                         menu_items = gtk.MenuItem(_("Quit"))
95                         filemenu.append(menu_items)
96                         menu_items.connect("activate", self._on_destroy, None)
97
98                         file_menu = gtk.MenuItem(_("File"))
99                         file_menu.show()
100                         file_menu.set_submenu(filemenu)
101
102                         categorymenu = gtk.Menu()
103
104                         menu_items = gtk.MenuItem(_("Delete"))
105                         categorymenu.append(menu_items)
106                         menu_items.connect("activate", self._on_delete_category)
107
108                         menu_items = gtk.MenuItem(_("Move To Category"))
109                         categorymenu.append(menu_items)
110                         menu_items.connect("activate", self._on_move_category)
111
112                         category_menu = gtk.MenuItem(_("Category"))
113                         category_menu.show()
114                         category_menu.set_submenu(categorymenu)
115
116                         viewmenu = gtk.Menu()
117
118                         menu_items = gtk.MenuItem(_("Word Wrap"))
119                         viewmenu.append(menu_items)
120                         menu_items.connect("activate", self._on_toggle_word_wrap)
121
122                         menu_items = gtk.MenuItem(_("History"))
123                         viewmenu.append(menu_items)
124                         menu_items.connect("activate", self._on_show_history)
125
126                         view_menu = gtk.MenuItem(_("Note"))
127                         view_menu.show()
128                         view_menu.set_submenu(viewmenu)
129
130                         helpmenu = gtk.Menu()
131
132                         menu_items = gtk.MenuItem(_("About"))
133                         helpmenu.append(menu_items)
134                         menu_items.connect("activate", self._on_show_about, None)
135
136                         help_menu = gtk.MenuItem(_("Help"))
137                         help_menu.show()
138                         help_menu.set_submenu(helpmenu)
139
140                         menuBar = gtk.MenuBar()
141                         menuBar.show()
142                         menuBar.append (file_menu)
143                         menuBar.append (category_menu)
144                         menuBar.append (view_menu)
145                         menuBar.append (help_menu)
146
147                         vbox.pack_start(menuBar, False, False, 0)
148                 else:
149                         menuBar = gtk.MenuBar()
150                         menuBar.show()
151                         vbox.pack_start(menuBar, False, False, 0)
152
153                 #Create GUI elements
154                 self._topBox = kopfzeile.Kopfzeile(self._db)
155                 vbox.pack_start(self._topBox, False, False, 0)
156
157                 self._notizen = notizen.Notizen(self._db, self._topBox)
158                 vbox.pack_start(self._notizen, True, True, 0)
159
160                 #Get the Main Window, and connect the "destroy" event
161                 self._window = gtk.Window()
162                 self._window.add(vbox)
163
164                 self._on_toggle_word_wrap()
165                 self.enable_zoom(True)
166
167                 try:
168                         os.makedirs(self._user_data)
169                 except OSError, e:
170                         if e.errno != 17:
171                                 raise
172
173                 self._window = hildonize.hildonize_window(self, self._window)
174                 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
175                 menuBar = hildonize.hildonize_menu(
176                         self._window,
177                         menuBar,
178                 )
179                 if hildonize.IS_FREMANTLE_SUPPORTED:
180                         moveToCategoryButton = gtk.Button(_("Move To Category"))
181                         moveToCategoryButton.connect("clicked", self._on_move_category)
182                         menuBar.append(moveToCategoryButton)
183
184                         deleteCategoryButton = gtk.Button(_("Delete Category"))
185                         deleteCategoryButton.connect("clicked", self._on_delete_category)
186                         menuBar.append(deleteCategoryButton)
187
188                         historyButton= gtk.Button(_("Note History"))
189                         historyButton.connect("clicked", self._on_show_history)
190                         menuBar.append(historyButton)
191
192                         menuBar.show_all()
193
194                 if not hildonize.IS_HILDON_SUPPORTED:
195                         _moduleLogger.info("No hildonization support")
196
197                 if osso is not None:
198                         self._osso_c = osso.Context(constants.__app_name__, constants.__version__, False)
199                         self._deviceState = osso.DeviceState(self._osso_c)
200                         self._deviceState.set_device_state_callback(self._on_device_state_change, 0)
201                 else:
202                         _moduleLogger.info("No osso support")
203                         self._osso_c = None
204                         self._deviceState = None
205
206                 self._prepare_sync_dialog()
207
208                 self._window.connect("delete-event", self._on_delete_event)
209                 self._window.connect("destroy", self._on_destroy)
210                 self._window.connect("key-press-event", self._on_key_press)
211                 self._window.connect("window-state-event", self._on_window_state_change)
212
213                 self._window.show_all()
214                 self._load_settings()
215
216         def main(self):
217                 gtk.main()
218
219         def _save_settings(self):
220                 config = ConfigParser.SafeConfigParser()
221                 self.save_settings(config)
222                 with open(self._user_settings, "wb") as configFile:
223                         config.write(configFile)
224
225         def save_settings(self, config):
226                 config.add_section(constants.__pretty_app_name__)
227                 config.set(constants.__pretty_app_name__, "wordwrap", str(self._wordWrapEnabled))
228                 config.set(constants.__pretty_app_name__, "zoom", str(self._isZoomEnabled))
229                 config.set(constants.__pretty_app_name__, "fullscreen", str(self._window_in_fullscreen))
230
231         def _load_settings(self):
232                 config = ConfigParser.SafeConfigParser()
233                 config.read(self._user_settings)
234                 self.load_settings(config)
235
236         def load_settings(self, config):
237                 try:
238                         self._wordWrapEnabled = config.getboolean(constants.__pretty_app_name__, "wordwrap")
239                         self._isZoomEnabled = config.getboolean(constants.__pretty_app_name__, "zoom")
240                         self._window_in_fullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
241                 except ConfigParser.NoSectionError, e:
242                         warnings.warn(
243                                 "Settings file %s is missing section %s" % (
244                                         self._user_settings,
245                                         e.section,
246                                 ),
247                                 stacklevel=2
248                         )
249
250                 self._notizen.set_wordwrap(self._wordWrapEnabled)
251
252                 self.enable_zoom(self._isZoomEnabled)
253
254                 if self._window_in_fullscreen:
255                         self._window.fullscreen()
256                 else:
257                         self._window.unfullscreen()
258
259         def set_db_file(self, widget = None, data = None):
260                 dlg = hildon.FileChooserDialog(parent=self._window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
261
262                 if self._db.ladeDirekt('datenbank'):
263                         dlg.set_filename(self._db.ladeDirekt('datenbank'))
264
265                 dlg.set_title(_("Choose database file"))
266                 if dlg.run() == gtk.RESPONSE_OK:
267                         fileName = dlg.get_filename()
268                         self._db.speichereDirekt('datenbank', fileName)
269
270                         self._db.openDB()
271                         self._topBox.load_categories()
272                         self._notizen.load_notes()
273                 dlg.destroy()
274
275         def _prepare_sync_dialog(self):
276                 self._syncDialog = gtk.Dialog(_("Sync"), None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
277
278                 self._syncDialog.set_position(gtk.WIN_POS_CENTER)
279                 syncer = sync.Sync(self._db, self._window, 50504)
280                 self._syncDialog.vbox.pack_start(syncer, True, True, 0)
281                 self._syncDialog.set_size_request(500, 350)
282                 self._syncDialog.vbox.show_all()
283                 syncer.connect("syncFinished", self._on_sync_finished)
284
285         def enable_zoom(self, zoomEnabled):
286                 self._isZoomEnabled = zoomEnabled
287                 if zoomEnabled:
288                         self._topBox.hide()
289                 else:
290                         self._topBox.show()
291
292         @gtk_toolbox.log_exception(_moduleLogger)
293         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
294                 """
295                 For system_inactivity, we have no background tasks to pause
296
297                 @note Hildon specific
298                 """
299                 if memory_low:
300                         gc.collect()
301
302                 if save_unsaved_data or shutdown:
303                         self._save_settings()
304
305         @gtk_toolbox.log_exception(_moduleLogger)
306         def _on_window_state_change(self, widget, event, *args):
307                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
308                         self._window_in_fullscreen = True
309                 else:
310                         self._window_in_fullscreen = False
311
312         @gtk_toolbox.log_exception(_moduleLogger)
313         def _on_key_press(self, widget, event, *args):
314                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
315                 isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
316                 if (
317                         event.keyval == gtk.keysyms.F6 or
318                         event.keyval in RETURN_TYPES and isCtrl
319                 ):
320                         # The "Full screen" hardware key has been pressed 
321                         if self._window_in_fullscreen:
322                                 self._window.unfullscreen ()
323                         else:
324                                 self._window.fullscreen ()
325                         return True
326                 elif (
327                         event.keyval == gtk.keysyms.F7 or
328                         event.keyval == gtk.keysyms.i and isCtrl
329                 ):
330                         # Zoom In
331                         self.enable_zoom(True)
332                         return True
333                 elif (
334                         event.keyval == gtk.keysyms.F8 or
335                         event.keyval == gtk.keysyms.o and isCtrl
336                 ):
337                         # Zoom Out
338                         self.enable_zoom(False)
339                         return True
340                 elif (
341                         event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and
342                         event.get_state() & gtk.gdk.CONTROL_MASK
343                 ):
344                         self._window.destroy()
345                 elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
346                         with open(constants._user_logpath_, "r") as f:
347                                 logLines = f.xreadlines()
348                                 log = "".join(logLines)
349                                 self._clipboard.set_text(str(log))
350                         return True
351
352         @gtk_toolbox.log_exception(_moduleLogger)
353         def _on_show_history(self, *args):
354                 self._notizen.show_history()
355
356         @gtk_toolbox.log_exception(_moduleLogger)
357         def _on_view_sql_history(self, widget = None, data = None, data2 = None):
358                 import sqldialog
359                 sqldiag = sqldialog.SqlDialog(self._db)
360                 res = sqldiag.run()
361                 sqldiag.hide()
362                 if res == sqldiag.EXPORT_RESPONSE:
363                         _moduleLogger.info("exporting sql")
364
365                         dlg = hildon.FileChooserDialog(parent=self._window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
366
367                         dlg.set_title(_("Select SQL export file"))
368                         if dlg.run() == gtk.RESPONSE_OK:
369                                 fileName = dlg.get_filename()
370                                 sqldiag.exportSQL(fileName)
371                         dlg.destroy()
372
373                 sqldiag.destroy()
374
375         @gtk_toolbox.log_exception(_moduleLogger)
376         def _on_move_category(self, *args):
377                 comboCategory = gtk.combo_box_new_text()
378                 comboCategory.append_text('undefined')
379                 sql = "SELECT id, liste FROM categories WHERE id = 0 ORDER BY liste"
380                 rows = self._db.ladeSQL(sql)
381                 for row in rows:
382                         comboCategory.append_text(row[1])
383
384                 dialog = gtk.Dialog(_("Choose category"), self._window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
385                 try:
386                         dialog.set_position(gtk.WIN_POS_CENTER)
387
388                         dialog.vbox.pack_start(comboCategory, True, True, 0)
389                         dialog.vbox.show_all()
390
391                         userResponse = dialog.run()
392                 finally:
393                         dialog.destroy()
394
395                 if userResponse != gtk.RESPONSE_ACCEPT:
396                         return
397
398                 n = comboCategory.get_active()
399                 if -1 < n and self._notizen.noteId != -1:
400                         model = comboCategory.get_model()
401                         active = comboCategory.get_active()
402                         if active < 0:
403                                 return None
404                         cat_id = model[active][0]
405
406                         noteid, pcdatum, category, note = self._db.loadNote(self._notizen.noteId)
407                         self._db.saveNote(noteid, note, cat_id, pcdatum = None)
408                         self._topBox.category_combo_changed()
409                 else:
410                         mbox = gtk.MessageDialog(self._window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("No note selected."))
411                         response = mbox.run()
412                         mbox.hide()
413                         mbox.destroy()
414
415         @gtk_toolbox.log_exception(_moduleLogger)
416         def _on_delete_category(self, *args):
417                 if self._topBox.get_category() == "%" or self._topBox.get_category() == "undefined":
418                         mbox = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("This category can not be deleted"))
419                         response = mbox.run()
420                         mbox.hide()
421                         mbox.destroy()
422                         return
423
424                 mbox = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_YES_NO, _("Are you sure to delete the current category?"))
425                 response = mbox.run()
426                 mbox.hide()
427                 mbox.destroy()
428                 if response == gtk.RESPONSE_YES:
429                         sql = "UPDATE notes SET category = ? WHERE category = ?"
430                         self._db.speichereSQL(sql, ("undefined", self._topBox.get_category()))
431                         sql = "DELETE FROM categories WHERE liste = ?"
432                         self._db.speichereSQL(sql, (self._topBox.get_category(), ))
433                         model = self._topBox.categoryCombo.get_model()
434                         pos = self._topBox.categoryCombo.get_active()
435                         if (pos>1):
436                                 self._topBox.categoryCombo.remove_text(pos)
437                                 self._topBox.categoryCombo.set_active(0)
438
439         @gtk_toolbox.log_exception(_moduleLogger)
440         def _on_sync_finished(self, data = None, data2 = None):
441                 self._topBox.load_categories()
442                 self._notizen.load_notes()
443
444         @gtk_toolbox.log_exception(_moduleLogger)
445         def _on_sync_notes(self, widget = None, data = None):
446                 self._syncDialog.run()
447                 self._syncDialog.hide()
448
449         @gtk_toolbox.log_exception(_moduleLogger)
450         def _on_toggle_word_wrap(self, *args):
451                 self._wordWrapEnabled = not self._wordWrapEnabled
452                 self._notizen.set_wordwrap(self._wordWrapEnabled)
453
454         @gtk_toolbox.log_exception(_moduleLogger)
455         def _on_delete_event(self, widget, event, data = None):
456                 return False
457
458         @gtk_toolbox.log_exception(_moduleLogger)
459         def _on_destroy(self, widget = None, data = None):
460                 try:
461                         self._db.close()
462                         self._save_settings()
463
464                         try:
465                                 self._deviceState.close()
466                         except AttributeError:
467                                 pass # Either None or close was removed (in Fremantle)
468                         try:
469                                 self._osso_c.close()
470                         except AttributeError:
471                                 pass # Either None or close was removed (in Fremantle)
472                 finally:
473                         gtk.main_quit()
474
475         @gtk_toolbox.log_exception(_moduleLogger)
476         def _on_show_about(self, widget = None, data = None):
477                 dialog = gtk.AboutDialog()
478                 dialog.set_position(gtk.WIN_POS_CENTER)
479                 dialog.set_name(constants.__pretty_app_name__)
480                 dialog.set_version(constants.__version__)
481                 dialog.set_copyright("")
482                 dialog.set_website("http://axique.de/index.php?f=Quicknote")
483                 comments = _("%s is a note taking program; it is optimised for quick save and search of notes") % constants.__pretty_app_name__
484                 dialog.set_comments(comments)
485                 dialog.set_authors(["Christoph Wurstle <n800@axique.net>", "Ed Page <eopage@byu.net> (Blame him for the most recent bugs)"])
486                 dialog.run()
487                 dialog.destroy()
488
489
490 def run_quicknote():
491         if hildonize.IS_HILDON_SUPPORTED:
492                 gtk.set_application_name(constants.__pretty_app_name__)
493         app = QuicknoteProgram()
494         app.main()
495
496
497 if __name__ == "__main__":
498         logging.basicConfig(level=logging.DEBUG)
499         run_quicknote()