17
16
# indicator-stickynotes. If not, see <http://www.gnu.org/licenses/>.
19
18
from datetime import datetime
21
from gi.repository import Gtk, Gdk, Gio, GObject, GtkSource
19
from string import Template
21
gi.require_version("Gtk", "3.0")
22
gi.require_version("GtkSource", "3.0")
23
from gi.repository import Gtk, Gdk, Gio, GObject, GtkSource, Pango
24
from locale import gettext as _
24
class StickyNote(object):
29
def load_global_css():
30
"""Adds a provider for the global CSS"""
31
global_css = Gtk.CssProvider()
32
global_css.load_from_path(os.path.join(os.path.dirname(__file__), "..",
34
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
35
global_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
38
"""Manages the GUI of an individual stickynote"""
26
39
def __init__(self, note):
40
"""Initializes the stickynotes window"""
27
41
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
44
self.noteset = note.noteset
30
45
self.locked = self.note.properties.get("locked", False)
48
self.menu = Gtk.Menu()
51
# Load CSS template and initialize Gtk.CssProvider
52
with open(os.path.join(self.path, "style.css"), encoding="utf-8") \
54
self.css_template = Template(css_file.read())
55
self.css = Gtk.CssProvider()
31
60
self.builder = Gtk.Builder()
32
61
GObject.type_register(GtkSource.View)
33
62
self.builder.add_from_file(os.path.join(self.path,
35
64
self.builder.connect_signals(self)
65
self.winMain = self.builder.get_object("MainWindow")
36
67
# Get necessary objects
37
self.txtNote = self.builder.get_object("txtNote")
38
self.winMain = self.builder.get_object("MainWindow")
39
self.winMain.set_name("main-window")
40
self.bAdd = self.builder.get_object("bAdd")
41
self.imgAdd = self.builder.get_object("imgAdd")
42
self.imgResizeR = self.builder.get_object("imgResizeR")
43
self.eResizeR = self.builder.get_object("eResizeR")
44
self.bLock = self.builder.get_object("bLock")
45
self.imgLock = self.builder.get_object("imgLock")
46
self.imgUnlock = self.builder.get_object("imgUnlock")
47
self.bClose = self.builder.get_object("bClose")
48
self.confirmDelete = self.builder.get_object("confirmDelete")
53
# (Maybe?) Remove this eventually
68
widgets = ["txtNote", "bAdd", "imgAdd", "imgResizeR", "eResizeR",
69
"bLock", "imgLock", "imgUnlock", "imgClose", "imgDropdown",
70
"bClose", "confirmDelete", "movebox1", "movebox2"]
72
setattr(self, w, self.builder.get_object(w))
73
self.style_contexts = [self.winMain.get_style_context(),
74
self.txtNote.get_style_context()]
75
# Update window-specific style. Global styles are loaded initially!
78
# Ensure buttons are displayed with images
54
79
settings = Gtk.Settings.get_default()
55
80
settings.props.gtk_button_images = True
57
css = Gtk.CssProvider()
58
css.load_from_file(Gio.File.new_for_path(os.path.join(self.path,
60
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
63
82
self.bbody = GtkSource.Buffer()
64
83
self.bbody.begin_not_undoable_action()
81
101
# Set locked state
82
102
self.set_locked_state(self.locked)
84
def show(self, widget=None, event=None):
85
self.winMain.present()
87
self.winMain.move(*self.note.properties.get("position", (10,10)))
104
# call set_keep_above just to have the note appearing
105
# above everything else.
106
# without it, it still won't appear above a window
107
# in which a cursor is active
108
self.winMain.set_keep_above(True)
110
# immediately undo the set keep above after the window
111
# is shown, so that windows won't stay up if we switch to
113
self.winMain.set_keep_above(False)
116
# (re-)show the sticky note after it has been hidden getting a sticky note
117
# to show itself was problematic after a "show desktop" command in unity.
118
# (see bug lp:1105948). Reappearance of dialog is problematic for any
119
# dialog which has the skip_taskbar_hint=True property in StickyNotes.ui
120
# (property necessary to prevent sticky note from showing on the taskbar)
122
# workaround which is based on deleting a sticky note and re-initializing
124
def show(self, widget=None, event=None, reload_from_backend=False):
125
"""Shows the stickynotes window"""
127
# don't overwrite settings if loading from backend
128
if not reload_from_backend:
129
# store sticky note's settings
132
# Categories may have changed in backend
135
# destroy its main window
136
self.winMain.destroy()
138
# reinitialize that window
89
141
def hide(self, *args):
142
"""Hides the stickynotes window"""
90
143
self.winMain.hide()
92
145
def update_note(self):
146
"""Update the underlying note object"""
93
147
self.note.update(self.bbody.get_text(self.bbody.get_start_iter(),
94
148
self.bbody.get_end_iter(), True))
96
150
def move(self, widget, event):
151
"""Action to begin moving (by dragging) the window"""
97
152
self.winMain.begin_move_drag(event.button, event.x_root,
98
153
event.y_root, event.get_time())
101
156
def resize(self, widget, event, *args):
157
"""Action to begin resizing (by dragging) the window"""
102
158
self.winMain.begin_resize_drag(Gdk.WindowEdge.SOUTH_EAST,
103
159
event.button, event.x_root, event.y_root, event.get_time())
106
162
def properties(self):
163
"""Get properties of the current note"""
107
164
prop = {"position":self.winMain.get_position(),
108
165
"size":self.winMain.get_size(), "locked":self.locked}
109
166
if not self.winMain.get_visible():
110
167
prop["position"] = self.note.properties.get("position", (10, 10))
111
prop["size"] = self.note.properties.get("size", (200, 200))
168
prop["size"] = self.note.properties.get("size", (200, 150))
171
def update_font(self):
172
"""Updates the font"""
173
# Unset any previously set font
174
self.txtNote.override_font(None)
175
font = Pango.FontDescription.from_string(
176
self.note.cat_prop("font"))
177
self.txtNote.override_font(font)
179
def update_style(self):
180
"""Updates the style using CSS template"""
181
self.update_button_color()
182
css_string = self.css_template.substitute(**self.css_data())\
183
.encode("ascii", "replace")
184
self.css.load_from_data(css_string)
185
for context in self.style_contexts:
186
context.add_provider(self.css,
187
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
189
def update_button_color(self):
190
"""Switches between regular and dark icons appropriately"""
191
h,s,v = self.note.cat_prop("bgcolor_hsv")
192
# an arbitrary quadratic found by trial and error
193
thresh_sat = 1.05 - 1.7*((v-1)**2)
194
suffix = "-dark" if s >= thresh_sat else ""
195
iconfiles = {"imgAdd":"add", "imgClose":"close", "imgDropdown":"menu",
196
"imgLock":"lock", "imgUnlock":"unlock", "imgResizeR":"resizer"}
197
for img, filename in iconfiles.items():
198
getattr(self, img).set_from_file(
199
os.path.join(os.path.dirname(__file__), "..","Icons/" +
200
filename + suffix + ".png"))
203
"""Returns data to substitute into the CSS template"""
205
# Converts to RGB hex. All RGB/HSV values are scaled to a max of 1
206
rgb_to_hex = lambda x: "#" + "".join(["{:02x}".format(int(255*a))
208
hsv_to_hex = lambda x: rgb_to_hex(colorsys.hsv_to_rgb(*x))
209
bgcolor_hsv = self.note.cat_prop("bgcolor_hsv")
210
data["bgcolor_hex"] = hsv_to_hex(
211
self.note.cat_prop("bgcolor_hsv"))
212
data["text_color"] = rgb_to_hex(self.note.cat_prop("textcolor"))
215
def populate_menu(self):
216
"""(Re)populates the note's menu items appropriately"""
217
def _delete_menu_item(item, *args):
218
self.menu.remove(item)
219
self.menu.foreach(_delete_menu_item, None)
221
aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
222
aot.connect("toggled", self.malways_on_top_toggled)
223
self.menu.append(aot)
226
mset = Gtk.MenuItem(_("Settings"))
227
mset.connect("activate", self.noteset.indicator.show_settings)
228
self.menu.append(mset)
231
sep = Gtk.SeparatorMenuItem()
232
self.menu.append(sep)
236
mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
238
self.menu.append(mcats)
239
mcats.set_sensitive(False)
240
catgroup = mcats.get_group()
243
for cid, cdata in self.noteset.categories.items():
244
mitem = Gtk.RadioMenuItem.new_with_label(catgroup,
245
cdata.get("name", _("New Category")))
246
catgroup = mitem.get_group()
247
if cid == self.note.category:
248
mitem.set_active(True)
249
mitem.connect("activate", self.set_category, cid)
250
self.menu.append(mitem)
253
def malways_on_top_toggled(self, widget, *args):
254
self.winMain.set_keep_above(widget.get_active())
114
256
def save(self, *args):
115
257
self.note.noteset.save()
118
260
def add(self, *args):
119
self.note.noteset.new()
261
new_note = self.note.noteset.new()
263
# Set the new note to the current category
264
new_note.gui.set_category(None, self.note.category)
265
new_note.gui.populate_menu() # Fix Category Menu Selected indicator
267
# Set the new note position below this note
268
w, h = self.note.properties.get("position", (10, 10))
269
h += self.winMain.get_allocation().height + 10
270
new_note.gui.winMain.move(w, h)
122
274
def delete(self, *args):
123
confirm = self.confirmDelete.run()
124
self.confirmDelete.hide()
275
winConfirm = Gtk.MessageDialog(self.winMain, None,
276
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
277
_("Are you sure you want to delete this note?"))
278
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
279
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
280
confirm = winConfirm.run()
282
if confirm == Gtk.ResponseType.ACCEPT:
126
283
self.note.delete()
284
self.winMain.destroy()
289
def popup_menu(self, button, *args):
290
"""Pops up the note's menu"""
291
self.menu.popup(None, None, None, None, Gdk.BUTTON_PRIMARY,
292
Gtk.get_current_event_time())
294
def set_category(self, widget, cat):
295
"""Set the note's category"""
296
if not cat in self.noteset.categories:
297
raise KeyError("No such category")
298
self.note.category = cat
132
302
def set_locked_state(self, locked):
303
"""Change the locked state of the stickynote"""
133
304
self.locked = locked
134
if not self.bLock.get_active() == self.locked:
135
self.bLock.set_active(self.locked)
136
305
self.txtNote.set_editable(not self.locked)
137
306
self.txtNote.set_cursor_visible(not self.locked)
138
307
self.bLock.set_image({True:self.imgLock,
139
308
False:self.imgUnlock}[self.locked])
141
def lock_toggled(self, *args):
142
self.set_locked_state(self.bLock.get_active())
144
def quit(self, *args):
309
self.bLock.set_tooltip_text({True: _("Unlock"),
310
False: _("Lock")}[self.locked])
312
def lock_clicked(self, *args):
313
"""Toggle the locked state of the note"""
314
self.set_locked_state(not self.locked)
147
316
def focus_out(self, *args):
319
def show_about_dialog():
320
glade_file = os.path.abspath(os.path.join(os.path.dirname(__file__),
321
'..', "GlobalDialogs.ui"))
322
builder = Gtk.Builder()
323
builder.add_from_file(glade_file)
324
winAbout = builder.get_object("AboutWindow")
329
class SettingsCategory:
330
"""Widgets that handle properties of a category"""
331
def __init__(self, settingsdialog, cat):
332
self.settingsdialog = settingsdialog
333
self.noteset = settingsdialog.noteset
335
self.builder = Gtk.Builder()
336
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
338
self.builder.add_objects_from_file(os.path.join(self.path,
339
"SettingsCategory.ui"), ["catExpander"])
340
self.builder.connect_signals(self)
341
widgets = ["catExpander", "lExp", "cbBG", "cbText", "eName",
342
"confirmDelete", "fbFont"]
344
setattr(self, w, self.builder.get_object(w))
345
name = self.noteset.categories[cat].get("name", _("New Category"))
346
self.eName.set_text(name)
348
self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
349
*self.noteset.get_category_property(cat, "bgcolor_hsv")),
351
self.cbText.set_rgba(Gdk.RGBA(
352
*self.noteset.get_category_property(cat, "textcolor"),
354
fontname = self.noteset.get_category_property(cat, "font")
356
# Get the system default font, if none is set
358
self.settingsdialog.wSettings.get_style_context()\
359
.get_font(Gtk.StateFlags.NORMAL).to_string()
360
#why.is.this.so.long?
361
self.fbFont.set_font(fontname)
363
def refresh_title(self, *args):
364
"""Updates the title of the category"""
365
name = self.noteset.categories[self.cat].get("name",
367
if self.noteset.properties.get("default_cat", "") == self.cat:
368
name += " (" + _("Default Category") + ")"
369
self.lExp.set_text(name)
371
def delete_cat(self, *args):
372
"""Delete a category"""
373
winConfirm = Gtk.MessageDialog(self.settingsdialog.wSettings, None,
374
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
375
_("Are you sure you want to delete this category?"))
376
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
377
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
378
confirm = winConfirm.run()
380
if confirm == Gtk.ResponseType.ACCEPT:
381
self.settingsdialog.delete_category(self.cat)
383
def make_default(self, *args):
384
"""Make this the default category"""
385
self.noteset.properties["default_cat"] = self.cat
386
self.settingsdialog.refresh_category_titles()
387
for note in self.noteset.notes:
388
note.gui.update_style()
389
note.gui.update_font()
391
def eName_changed(self, *args):
392
"""Update a category name"""
393
self.noteset.categories[self.cat]["name"] = self.eName.get_text()
395
for note in self.noteset.notes:
396
note.gui.populate_menu()
398
def update_bg(self, *args):
399
"""Action to update the background color"""
401
rgba = self.cbBG.get_rgba()
404
self.cbBG.get_rgba(rgba)
405
# Some versions of GObjectIntrospection are affected by
406
# https://bugzilla.gnome.org/show_bug.cgi?id=687633
407
hsv = colorsys.rgb_to_hsv(rgba.red, rgba.green, rgba.blue)
408
self.noteset.categories[self.cat]["bgcolor_hsv"] = hsv
409
for note in self.noteset.notes:
410
note.gui.update_style()
411
# Remind some widgets that they are transparent, etc.
414
def update_textcolor(self, *args):
415
"""Action to update the text color"""
417
rgba = self.cbText.get_rgba()
420
self.cbText.get_rgba(rgba)
421
self.noteset.categories[self.cat]["textcolor"] = \
422
[rgba.red, rgba.green, rgba.blue]
423
for note in self.noteset.notes:
424
note.gui.update_style()
426
def update_font(self, *args):
427
"""Action to update the font size"""
428
self.noteset.categories[self.cat]["font"] = \
429
self.fbFont.get_font_name()
430
for note in self.noteset.notes:
431
note.gui.update_font()
433
class SettingsDialog:
434
"""Manages the GUI of the settings dialog"""
435
def __init__(self, noteset):
436
self.noteset = noteset
438
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
440
glade_file = (os.path.join(self.path, "GlobalDialogs.ui"))
441
self.builder = Gtk.Builder()
442
self.builder.add_from_file(glade_file)
443
self.builder.connect_signals(self)
444
widgets = ["wSettings", "boxCategories"]
446
setattr(self, w, self.builder.get_object(w))
447
for c in self.noteset.categories:
448
self.add_category_widgets(c)
449
ret = self.wSettings.run()
450
self.wSettings.destroy()
452
def add_category_widgets(self, cat):
453
"""Add the widgets for a category"""
454
self.categories[cat] = SettingsCategory(self, cat)
455
self.boxCategories.pack_start(self.categories[cat].catExpander,
458
def new_category(self, *args):
459
"""Make a new category"""
460
cid = str(uuid.uuid4())
461
self.noteset.categories[cid] = {}
462
self.add_category_widgets(cid)
464
def delete_category(self, cat):
465
"""Delete a category"""
466
del self.noteset.categories[cat]
467
self.categories[cat].catExpander.destroy()
468
del self.categories[cat]
469
for note in self.noteset.notes:
470
note.gui.populate_menu()
471
note.gui.update_style()
472
note.gui.update_font()
474
def refresh_category_titles(self):
475
for cid, catsettings in self.categories.items():
476
catsettings.refresh_title()