1
# Copyright © 2012-2015 Umang Varma <umang.me@gmail.com>
3
# This file is part of indicator-stickynotes.
5
# indicator-stickynotes is free software: you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or (at your
8
# option) any later version.
10
# indicator-stickynotes is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15
# You should have received a copy of the GNU General Public License along with
16
# indicator-stickynotes. If not, see <http://www.gnu.org/licenses/>.
18
from datetime import datetime
19
from string import Template
20
from gi.repository import Gtk, Gdk, Gio, GObject, GtkSource, Pango
21
from locale import gettext as _
26
def load_global_css():
27
"""Adds a provider for the global CSS"""
28
global_css = Gtk.CssProvider()
29
global_css.load_from_path(os.path.join(os.path.dirname(__file__), "..",
31
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
32
global_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
35
"""Manages the GUI of an individual stickynote"""
36
def __init__(self, note):
37
"""Initializes the stickynotes window"""
38
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
41
self.noteset = note.noteset
42
self.locked = self.note.properties.get("locked", False)
45
self.menu = Gtk.Menu()
48
# Load CSS template and initialize Gtk.CssProvider
49
with open(os.path.join(self.path, "style.css"), encoding="utf-8") \
51
self.css_template = Template(css_file.read())
52
self.css = Gtk.CssProvider()
57
self.builder = Gtk.Builder()
58
GObject.type_register(GtkSource.View)
59
self.builder.add_from_file(os.path.join(self.path,
61
self.builder.connect_signals(self)
62
self.winMain = self.builder.get_object("MainWindow")
64
# Get necessary objects
65
self.winMain.set_name("main-window")
66
widgets = ["txtNote", "bAdd", "imgAdd", "imgResizeR", "eResizeR",
67
"bLock", "imgLock", "imgUnlock", "imgClose", "imgDropdown",
68
"bClose", "confirmDelete", "movebox1", "movebox2"]
70
setattr(self, w, self.builder.get_object(w))
71
self.style_contexts = [self.winMain.get_style_context(),
72
self.txtNote.get_style_context()]
73
# Update window-specific style. Global styles are loaded initially!
76
# Ensure buttons are displayed with images
77
settings = Gtk.Settings.get_default()
78
settings.props.gtk_button_images = True
80
self.bbody = GtkSource.Buffer()
81
self.bbody.begin_not_undoable_action()
82
self.bbody.set_text(self.note.body)
83
self.bbody.set_highlight_matching_brackets(False)
84
self.bbody.end_not_undoable_action()
85
self.txtNote.set_buffer(self.bbody)
87
self.winMain.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
88
self.eResizeR.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
90
self.winMain.move(*self.note.properties.get("position", (10,10)))
91
self.winMain.resize(*self.note.properties.get("size", (200,150)))
93
self.winMain.set_skip_pager_hint(True)
94
self.winMain.show_all()
96
self.eResizeR.get_window().set_cursor(Gdk.Cursor.new_for_display(
97
self.eResizeR.get_window().get_display(),
98
Gdk.CursorType.BOTTOM_RIGHT_CORNER))
100
self.set_locked_state(self.locked)
102
# call set_keep_above just to have the note appearing
103
# above everything else.
104
# without it, it still won't appear above a window
105
# in which a cursor is active
106
self.winMain.set_keep_above(True)
108
# immediately undo the set keep above after the window
109
# is shown, so that windows won't stay up if we switch to
111
self.winMain.set_keep_above(False)
114
# (re-)show the sticky note after it has been hidden getting a sticky note
115
# to show itself was problematic after a "show desktop" command in unity.
116
# (see bug lp:1105948). Reappearance of dialog is problematic for any
117
# dialog which has the skip_taskbar_hint=True property in StickyNotes.glade
118
# (property necessary to prevent sticky note from showing on the taskbar)
120
# workaround which is based on deleting a sticky note and re-initializing
122
def show(self, widget=None, event=None, reload_from_backend=False):
123
"""Shows the stickynotes window"""
125
# don't overwrite settings if loading from backend
126
if not reload_from_backend:
127
# store sticky note's settings
130
# Categories may have changed in backend
133
# destroy its main window
134
self.winMain.destroy()
136
# reinitialize that window
139
def hide(self, *args):
140
"""Hides the stickynotes window"""
143
def update_note(self):
144
"""Update the underlying note object"""
145
self.note.update(self.bbody.get_text(self.bbody.get_start_iter(),
146
self.bbody.get_end_iter(), True))
148
def move(self, widget, event):
149
"""Action to begin moving (by dragging) the window"""
150
self.winMain.begin_move_drag(event.button, event.x_root,
151
event.y_root, event.get_time())
154
def resize(self, widget, event, *args):
155
"""Action to begin resizing (by dragging) the window"""
156
self.winMain.begin_resize_drag(Gdk.WindowEdge.SOUTH_EAST,
157
event.button, event.x_root, event.y_root, event.get_time())
160
def properties(self):
161
"""Get properties of the current note"""
162
prop = {"position":self.winMain.get_position(),
163
"size":self.winMain.get_size(), "locked":self.locked}
164
if not self.winMain.get_visible():
165
prop["position"] = self.note.properties.get("position", (10, 10))
166
prop["size"] = self.note.properties.get("size", (200, 150))
169
def update_font(self):
170
"""Updates the font"""
171
# Unset any previously set font
172
self.txtNote.override_font(None)
173
font = Pango.FontDescription.from_string(
174
self.note.cat_prop("font"))
175
self.txtNote.override_font(font)
177
def update_style(self):
178
"""Updates the style using CSS template"""
179
self.update_button_color()
180
css_string = self.css_template.substitute(**self.css_data())\
181
.encode("ascii", "replace")
182
self.css.load_from_data(css_string)
183
for context in self.style_contexts:
184
context.add_provider(self.css,
185
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
187
def update_button_color(self):
188
"""Switches between regular and dark icons appropriately"""
189
h,s,v = self.note.cat_prop("bgcolor_hsv")
190
# an arbitrary quadratic found by trial and error
191
thresh_sat = 1.05 - 1.7*((v-1)**2)
192
suffix = "-dark" if s >= thresh_sat else ""
193
iconfiles = {"imgAdd":"add", "imgClose":"close", "imgDropdown":"menu",
194
"imgLock":"lock", "imgUnlock":"unlock", "imgResizeR":"resizer"}
195
for img, filename in iconfiles.items():
196
getattr(self, img).set_from_file(
197
os.path.join(os.path.dirname(__file__), "..","Icons/" +
198
filename + suffix + ".png"))
201
"""Returns data to substitute into the CSS template"""
203
# Converts to RGB hex. All RGB/HSV values are scaled to a max of 1
204
rgb_to_hex = lambda x: "#" + "".join(["{:02x}".format(int(255*a))
206
hsv_to_hex = lambda x: rgb_to_hex(colorsys.hsv_to_rgb(*x))
207
bgcolor_hsv = self.note.cat_prop("bgcolor_hsv")
208
data["bgcolor_hex"] = hsv_to_hex(
209
self.note.cat_prop("bgcolor_hsv"))
210
data["text_color"] = rgb_to_hex(self.note.cat_prop("textcolor"))
213
def populate_menu(self):
214
"""(Re)populates the note's menu items appropriately"""
215
def _delete_menu_item(item, *args):
216
self.menu.remove(item)
217
self.menu.foreach(_delete_menu_item, None)
219
aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
220
aot.connect("toggled", self.malways_on_top_toggled)
221
self.menu.append(aot)
224
mset = Gtk.MenuItem(_("Settings"))
225
mset.connect("activate", self.noteset.indicator.show_settings)
226
self.menu.append(mset)
229
sep = Gtk.SeparatorMenuItem()
230
self.menu.append(sep)
234
mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
236
self.menu.append(mcats)
237
mcats.set_sensitive(False)
238
catgroup = mcats.get_group()
241
for cid, cdata in self.noteset.categories.items():
242
mitem = Gtk.RadioMenuItem.new_with_label(catgroup,
243
cdata.get("name", _("New Category")))
244
catgroup = mitem.get_group()
245
if cid == self.note.category:
246
mitem.set_active(True)
247
mitem.connect("activate", self.set_category, cid)
248
self.menu.append(mitem)
251
def malways_on_top_toggled(self, widget, *args):
252
self.winMain.set_keep_above(widget.get_active())
254
def save(self, *args):
255
self.note.noteset.save()
258
def add(self, *args):
259
self.note.noteset.new()
262
def delete(self, *args):
263
winConfirm = Gtk.MessageDialog(self.winMain, None,
264
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
265
_("Are you sure you want to delete this note?"))
266
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
267
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
268
confirm = winConfirm.run()
270
if confirm == Gtk.ResponseType.ACCEPT:
272
self.winMain.destroy()
277
def popup_menu(self, button, *args):
278
"""Pops up the note's menu"""
279
self.menu.popup(None, None, None, None, Gdk.BUTTON_PRIMARY,
280
Gtk.get_current_event_time())
282
def set_category(self, widget, cat):
283
"""Set the note's category"""
284
if not cat in self.noteset.categories:
285
raise KeyError("No such category")
286
self.note.category = cat
290
def set_locked_state(self, locked):
291
"""Change the locked state of the stickynote"""
293
self.txtNote.set_editable(not self.locked)
294
self.txtNote.set_cursor_visible(not self.locked)
295
self.bLock.set_image({True:self.imgLock,
296
False:self.imgUnlock}[self.locked])
297
self.bLock.set_tooltip_text({True: _("Unlock"),
298
False: _("Lock")}[self.locked])
300
def lock_clicked(self, *args):
301
"""Toggle the locked state of the note"""
302
self.set_locked_state(not self.locked)
304
def focus_out(self, *args):
307
def show_about_dialog():
308
glade_file = os.path.abspath(os.path.join(os.path.dirname(__file__),
309
'..', "GlobalDialogs.glade"))
310
builder = Gtk.Builder()
311
builder.add_from_file(glade_file)
312
winAbout = builder.get_object("AboutWindow")
317
class SettingsCategory:
318
"""Widgets that handle properties of a category"""
319
def __init__(self, settingsdialog, cat):
320
self.settingsdialog = settingsdialog
321
self.noteset = settingsdialog.noteset
323
self.builder = Gtk.Builder()
324
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
326
self.builder.add_objects_from_file(os.path.join(self.path,
327
"SettingsCategory.glade"), ["catExpander"])
328
self.builder.connect_signals(self)
329
widgets = ["catExpander", "lExp", "cbBG", "cbText", "eName",
330
"confirmDelete", "fbFont"]
332
setattr(self, w, self.builder.get_object(w))
333
name = self.noteset.categories[cat].get("name", _("New Category"))
334
self.eName.set_text(name)
336
self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
337
*self.noteset.get_category_property(cat, "bgcolor_hsv")),
339
self.cbText.set_rgba(Gdk.RGBA(
340
*self.noteset.get_category_property(cat, "textcolor"),
342
fontname = self.noteset.get_category_property(cat, "font")
344
# Get the system default font, if none is set
346
self.settingsdialog.wSettings.get_style_context()\
347
.get_font(Gtk.StateFlags.NORMAL).to_string()
348
#why.is.this.so.long?
349
self.fbFont.set_font(fontname)
351
def refresh_title(self, *args):
352
"""Updates the title of the category"""
353
name = self.noteset.categories[self.cat].get("name",
355
if self.noteset.properties.get("default_cat", "") == self.cat:
356
name += " (" + _("Default Category") + ")"
357
self.lExp.set_text(name)
359
def delete_cat(self, *args):
360
"""Delete a category"""
361
winConfirm = Gtk.MessageDialog(self.settingsdialog.wSettings, None,
362
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
363
_("Are you sure you want to delete this category?"))
364
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
365
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
366
confirm = winConfirm.run()
368
if confirm == Gtk.ResponseType.ACCEPT:
369
self.settingsdialog.delete_category(self.cat)
371
def make_default(self, *args):
372
"""Make this the default category"""
373
self.noteset.properties["default_cat"] = self.cat
374
self.settingsdialog.refresh_category_titles()
375
for note in self.noteset.notes:
376
note.gui.update_style()
377
note.gui.update_font()
379
def eName_changed(self, *args):
380
"""Update a category name"""
381
self.noteset.categories[self.cat]["name"] = self.eName.get_text()
383
for note in self.noteset.notes:
384
note.gui.populate_menu()
386
def update_bg(self, *args):
387
"""Action to update the background color"""
389
rgba = self.cbBG.get_rgba()
392
self.cbBG.get_rgba(rgba)
393
# Some versions of GObjectIntrospection are affected by
394
# https://bugzilla.gnome.org/show_bug.cgi?id=687633
395
hsv = colorsys.rgb_to_hsv(rgba.red, rgba.green, rgba.blue)
396
self.noteset.categories[self.cat]["bgcolor_hsv"] = hsv
397
for note in self.noteset.notes:
398
note.gui.update_style()
399
# Remind some widgets that they are transparent, etc.
402
def update_textcolor(self, *args):
403
"""Action to update the text color"""
405
rgba = self.cbText.get_rgba()
408
self.cbText.get_rgba(rgba)
409
self.noteset.categories[self.cat]["textcolor"] = \
410
[rgba.red, rgba.green, rgba.blue]
411
for note in self.noteset.notes:
412
note.gui.update_style()
414
def update_font(self, *args):
415
"""Action to update the font size"""
416
self.noteset.categories[self.cat]["font"] = \
417
self.fbFont.get_font_name()
418
for note in self.noteset.notes:
419
note.gui.update_font()
421
class SettingsDialog:
422
"""Manages the GUI of the settings dialog"""
423
def __init__(self, noteset):
424
self.noteset = noteset
426
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
428
glade_file = (os.path.join(self.path, "GlobalDialogs.glade"))
429
self.builder = Gtk.Builder()
430
self.builder.add_from_file(glade_file)
431
self.builder.connect_signals(self)
432
widgets = ["wSettings", "boxCategories"]
434
setattr(self, w, self.builder.get_object(w))
435
for c in self.noteset.categories:
436
self.add_category_widgets(c)
437
ret = self.wSettings.run()
438
self.wSettings.destroy()
440
def add_category_widgets(self, cat):
441
"""Add the widgets for a category"""
442
self.categories[cat] = SettingsCategory(self, cat)
443
self.boxCategories.pack_start(self.categories[cat].catExpander,
446
def new_category(self, *args):
447
"""Make a new category"""
448
cid = str(uuid.uuid4())
449
self.noteset.categories[cid] = {}
450
self.add_category_widgets(cid)
452
def delete_category(self, cat):
453
"""Delete a category"""
454
del self.noteset.categories[cat]
455
self.categories[cat].catExpander.destroy()
456
del self.categories[cat]
457
for note in self.noteset.notes:
458
note.gui.populate_menu()
459
note.gui.update_style()
460
note.gui.update_font()
462
def refresh_category_titles(self):
463
for cid, catsettings in self.categories.items():
464
catsettings.refresh_title()