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("Icons/" + filename + suffix +
200
"""Returns data to substitute into the CSS template"""
202
# Converts to RGB hex. All RGB/HSV values are scaled to a max of 1
203
rgb_to_hex = lambda x: "#" + "".join(["{:02x}".format(int(255*a))
205
hsv_to_hex = lambda x: rgb_to_hex(colorsys.hsv_to_rgb(*x))
206
bgcolor_hsv = self.note.cat_prop("bgcolor_hsv")
207
data["bgcolor_hex"] = hsv_to_hex(
208
self.note.cat_prop("bgcolor_hsv"))
209
data["text_color"] = rgb_to_hex(self.note.cat_prop("textcolor"))
212
def populate_menu(self):
213
"""(Re)populates the note's menu items appropriately"""
214
def _delete_menu_item(item, *args):
215
self.menu.remove(item)
216
self.menu.foreach(_delete_menu_item, None)
218
aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
219
aot.connect("toggled", self.malways_on_top_toggled)
220
self.menu.append(aot)
223
mset = Gtk.MenuItem(_("Settings"))
224
mset.connect("activate", self.noteset.indicator.show_settings)
225
self.menu.append(mset)
228
sep = Gtk.SeparatorMenuItem()
229
self.menu.append(sep)
233
mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
235
self.menu.append(mcats)
236
mcats.set_sensitive(False)
237
catgroup = mcats.get_group()
240
for cid, cdata in self.noteset.categories.items():
241
mitem = Gtk.RadioMenuItem.new_with_label(catgroup,
242
cdata.get("name", _("New Category")))
243
catgroup = mitem.get_group()
244
if cid == self.note.category:
245
mitem.set_active(True)
246
mitem.connect("activate", self.set_category, cid)
247
self.menu.append(mitem)
250
def malways_on_top_toggled(self, widget, *args):
251
self.winMain.set_keep_above(widget.get_active())
253
def save(self, *args):
254
self.note.noteset.save()
257
def add(self, *args):
258
self.note.noteset.new()
261
def delete(self, *args):
262
winConfirm = Gtk.MessageDialog(self.winMain, None,
263
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
264
_("Are you sure you want to delete this note?"))
265
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
266
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
267
confirm = winConfirm.run()
269
if confirm == Gtk.ResponseType.ACCEPT:
271
self.winMain.destroy()
276
def popup_menu(self, button, *args):
277
"""Pops up the note's menu"""
278
self.menu.popup(None, None, None, None, Gdk.BUTTON_PRIMARY,
279
Gtk.get_current_event_time())
281
def set_category(self, widget, cat):
282
"""Set the note's category"""
283
if not cat in self.noteset.categories:
284
raise KeyError("No such category")
285
self.note.category = cat
289
def set_locked_state(self, locked):
290
"""Change the locked state of the stickynote"""
292
self.txtNote.set_editable(not self.locked)
293
self.txtNote.set_cursor_visible(not self.locked)
294
self.bLock.set_image({True:self.imgLock,
295
False:self.imgUnlock}[self.locked])
296
self.bLock.set_tooltip_text({True: _("Unlock"),
297
False: _("Lock")}[self.locked])
299
def lock_clicked(self, *args):
300
"""Toggle the locked state of the note"""
301
self.set_locked_state(not self.locked)
303
def focus_out(self, *args):
306
def show_about_dialog():
307
glade_file = os.path.abspath(os.path.join(os.path.dirname(__file__),
308
'..', "GlobalDialogs.glade"))
309
builder = Gtk.Builder()
310
builder.add_from_file(glade_file)
311
winAbout = builder.get_object("AboutWindow")
316
class SettingsCategory:
317
"""Widgets that handle properties of a category"""
318
def __init__(self, settingsdialog, cat):
319
self.settingsdialog = settingsdialog
320
self.noteset = settingsdialog.noteset
322
self.builder = Gtk.Builder()
323
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
325
self.builder.add_objects_from_file(os.path.join(self.path,
326
"SettingsCategory.glade"), ["catExpander"])
327
self.builder.connect_signals(self)
328
widgets = ["catExpander", "lExp", "cbBG", "cbText", "eName",
329
"confirmDelete", "fbFont"]
331
setattr(self, w, self.builder.get_object(w))
332
name = self.noteset.categories[cat].get("name", _("New Category"))
333
self.eName.set_text(name)
335
self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
336
*self.noteset.get_category_property(cat, "bgcolor_hsv")),
338
self.cbText.set_rgba(Gdk.RGBA(
339
*self.noteset.get_category_property(cat, "textcolor"),
341
fontname = self.noteset.get_category_property(cat, "font")
343
# Get the system default font, if none is set
345
self.settingsdialog.wSettings.get_style_context()\
346
.get_font(Gtk.StateFlags.NORMAL).to_string()
347
#why.is.this.so.long?
348
self.fbFont.set_font(fontname)
350
def refresh_title(self, *args):
351
"""Updates the title of the category"""
352
name = self.noteset.categories[self.cat].get("name",
354
if self.noteset.properties.get("default_cat", "") == self.cat:
355
name += " (" + _("Default Category") + ")"
356
self.lExp.set_text(name)
358
def delete_cat(self, *args):
359
"""Delete a category"""
360
winConfirm = Gtk.MessageDialog(self.settingsdialog.wSettings, None,
361
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
362
_("Are you sure you want to delete this category?"))
363
winConfirm.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
364
Gtk.STOCK_DELETE, Gtk.ResponseType.ACCEPT)
365
confirm = winConfirm.run()
367
if confirm == Gtk.ResponseType.ACCEPT:
368
self.settingsdialog.delete_category(self.cat)
370
def make_default(self, *args):
371
"""Make this the default category"""
372
self.noteset.properties["default_cat"] = self.cat
373
self.settingsdialog.refresh_category_titles()
374
for note in self.noteset.notes:
375
note.gui.update_style()
376
note.gui.update_font()
378
def eName_changed(self, *args):
379
"""Update a category name"""
380
self.noteset.categories[self.cat]["name"] = self.eName.get_text()
382
for note in self.noteset.notes:
383
note.gui.populate_menu()
385
def update_bg(self, *args):
386
"""Action to update the background color"""
388
rgba = self.cbBG.get_rgba()
391
self.cbBG.get_rgba(rgba)
392
# Some versions of GObjectIntrospection are affected by
393
# https://bugzilla.gnome.org/show_bug.cgi?id=687633
394
hsv = colorsys.rgb_to_hsv(rgba.red, rgba.green, rgba.blue)
395
self.noteset.categories[self.cat]["bgcolor_hsv"] = hsv
396
for note in self.noteset.notes:
397
note.gui.update_style()
398
# Remind some widgets that they are transparent, etc.
401
def update_textcolor(self, *args):
402
"""Action to update the text color"""
404
rgba = self.cbText.get_rgba()
407
self.cbText.get_rgba(rgba)
408
self.noteset.categories[self.cat]["textcolor"] = \
409
[rgba.red, rgba.green, rgba.blue]
410
for note in self.noteset.notes:
411
note.gui.update_style()
413
def update_font(self, *args):
414
"""Action to update the font size"""
415
self.noteset.categories[self.cat]["font"] = \
416
self.fbFont.get_font_name()
417
for note in self.noteset.notes:
418
note.gui.update_font()
420
class SettingsDialog:
421
"""Manages the GUI of the settings dialog"""
422
def __init__(self, noteset):
423
self.noteset = noteset
425
self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
427
glade_file = (os.path.join(self.path, "GlobalDialogs.glade"))
428
self.builder = Gtk.Builder()
429
self.builder.add_from_file(glade_file)
430
self.builder.connect_signals(self)
431
widgets = ["wSettings", "boxCategories"]
433
setattr(self, w, self.builder.get_object(w))
434
for c in self.noteset.categories:
435
self.add_category_widgets(c)
436
ret = self.wSettings.run()
437
self.wSettings.destroy()
439
def add_category_widgets(self, cat):
440
"""Add the widgets for a category"""
441
self.categories[cat] = SettingsCategory(self, cat)
442
self.boxCategories.pack_start(self.categories[cat].catExpander,
445
def new_category(self, *args):
446
"""Make a new category"""
447
cid = str(uuid.uuid4())
448
self.noteset.categories[cid] = {}
449
self.add_category_widgets(cid)
451
def delete_category(self, cat):
452
"""Delete a category"""
453
del self.noteset.categories[cat]
454
self.categories[cat].catExpander.destroy()
455
del self.categories[cat]
456
for note in self.noteset.notes:
457
note.gui.populate_menu()
458
note.gui.update_style()
459
note.gui.update_font()
461
def refresh_category_titles(self):
462
for cid, catsettings in self.categories.items():
463
catsettings.refresh_title()