~umang/indicator-stickynotes/trunk

« back to all changes in this revision

Viewing changes to stickynotes/gui.py

  • Committer: Umang Varma
  • Date: 2015-07-11 20:23:33 UTC
  • Revision ID: git-v1:17b868aeb1031ade9f7d4bb35b628dc3e4f71aa0
Updated Italian translations

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright © 2012-2015 Umang Varma <umang.me@gmail.com>
 
2
 
3
# This file is part of indicator-stickynotes.
 
4
 
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.
 
9
 
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
 
13
# more details.
 
14
 
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/>.
 
17
 
 
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 _
 
22
import os.path
 
23
import colorsys
 
24
import uuid
 
25
 
 
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__), "..",
 
30
        "style_global.css"))
 
31
    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
 
32
            global_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
 
33
 
 
34
class StickyNote:
 
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__),
 
39
            '..'))
 
40
        self.note = note
 
41
        self.noteset = note.noteset
 
42
        self.locked = self.note.properties.get("locked", False)
 
43
 
 
44
        # Create menu
 
45
        self.menu = Gtk.Menu()
 
46
        self.populate_menu()
 
47
 
 
48
        # Load CSS template and initialize Gtk.CssProvider
 
49
        with open(os.path.join(self.path, "style.css"), encoding="utf-8") \
 
50
                as css_file:
 
51
            self.css_template = Template(css_file.read())
 
52
        self.css = Gtk.CssProvider()
 
53
 
 
54
        self.build_note()
 
55
        
 
56
    def build_note(self):
 
57
        self.builder = Gtk.Builder()
 
58
        GObject.type_register(GtkSource.View)
 
59
        self.builder.add_from_file(os.path.join(self.path,
 
60
            "StickyNotes.glade"))
 
61
        self.builder.connect_signals(self)
 
62
        self.winMain = self.builder.get_object("MainWindow")
 
63
 
 
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"]
 
69
        for w in widgets:
 
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!
 
74
        self.update_style()
 
75
        self.update_font()
 
76
        # Ensure buttons are displayed with images
 
77
        settings = Gtk.Settings.get_default()
 
78
        settings.props.gtk_button_images = True
 
79
        # Set text buffer
 
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)
 
86
        # Make resize work
 
87
        self.winMain.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
 
88
        self.eResizeR.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
 
89
        # Move Window
 
90
        self.winMain.move(*self.note.properties.get("position", (10,10)))
 
91
        self.winMain.resize(*self.note.properties.get("size", (200,150)))
 
92
        # Show the window
 
93
        self.winMain.set_skip_pager_hint(True)
 
94
        self.winMain.show_all()
 
95
        # Mouse over
 
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))
 
99
        # Set locked state
 
100
        self.set_locked_state(self.locked)
 
101
 
 
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)
 
107
 
 
108
        # immediately undo the set keep above after the window
 
109
        # is shown, so that windows won't stay up if we switch to
 
110
        # a different window
 
111
        self.winMain.set_keep_above(False)
 
112
 
 
113
 
 
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)
 
119
 
 
120
    # workaround which is based on deleting a sticky note and re-initializing
 
121
    # it. 
 
122
    def show(self, widget=None, event=None, reload_from_backend=False):
 
123
        """Shows the stickynotes window"""
 
124
 
 
125
        # don't overwrite settings if loading from backend
 
126
        if not reload_from_backend:
 
127
            # store sticky note's settings
 
128
            self.update_note()
 
129
        else:
 
130
            # Categories may have changed in backend
 
131
            self.populate_menu()
 
132
 
 
133
        # destroy its main window
 
134
        self.winMain.destroy()
 
135
 
 
136
        # reinitialize that window
 
137
        self.build_note()
 
138
 
 
139
    def hide(self, *args):
 
140
        """Hides the stickynotes window"""
 
141
        self.winMain.hide()
 
142
 
 
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))
 
147
 
 
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())
 
152
        return False
 
153
 
 
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())
 
158
        return True
 
159
 
 
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))
 
167
        return prop
 
168
 
 
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)
 
176
 
 
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)
 
186
 
 
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"))
 
199
 
 
200
    def css_data(self):
 
201
        """Returns data to substitute into the CSS template"""
 
202
        data = {}
 
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))
 
205
            for a in x])
 
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"))
 
211
        return data
 
212
 
 
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)
 
218
 
 
219
        aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
 
220
        aot.connect("toggled", self.malways_on_top_toggled)
 
221
        self.menu.append(aot)
 
222
        aot.show()
 
223
 
 
224
        mset = Gtk.MenuItem(_("Settings"))
 
225
        mset.connect("activate", self.noteset.indicator.show_settings)
 
226
        self.menu.append(mset)
 
227
        mset.show()
 
228
 
 
229
        sep = Gtk.SeparatorMenuItem()
 
230
        self.menu.append(sep)
 
231
        sep.show()
 
232
 
 
233
        catgroup = []
 
234
        mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
 
235
                _("Categories:"))
 
236
        self.menu.append(mcats)
 
237
        mcats.set_sensitive(False)
 
238
        catgroup = mcats.get_group()
 
239
        mcats.show()
 
240
 
 
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)
 
249
            mitem.show()
 
250
 
 
251
    def malways_on_top_toggled(self, widget, *args):
 
252
        self.winMain.set_keep_above(widget.get_active())
 
253
 
 
254
    def save(self, *args):
 
255
        self.note.noteset.save()
 
256
        return False
 
257
 
 
258
    def add(self, *args):
 
259
        self.note.noteset.new()
 
260
        return False
 
261
 
 
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()
 
269
        winConfirm.destroy()
 
270
        if confirm == Gtk.ResponseType.ACCEPT:
 
271
            self.note.delete()
 
272
            self.winMain.destroy()
 
273
            return False
 
274
        else:
 
275
            return True
 
276
 
 
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())
 
281
 
 
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
 
287
        self.update_style()
 
288
        self.update_font()
 
289
 
 
290
    def set_locked_state(self, locked):
 
291
        """Change the locked state of the stickynote"""
 
292
        self.locked = locked
 
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])
 
299
 
 
300
    def lock_clicked(self, *args):
 
301
        """Toggle the locked state of the note"""
 
302
        self.set_locked_state(not self.locked)
 
303
 
 
304
    def focus_out(self, *args):
 
305
        self.save(*args)
 
306
 
 
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")
 
313
    ret =  winAbout.run()
 
314
    winAbout.destroy()
 
315
    return ret
 
316
 
 
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
 
322
        self.cat = cat
 
323
        self.builder = Gtk.Builder()
 
324
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
 
325
            '..'))
 
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"]
 
331
        for w in widgets:
 
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)
 
335
        self.refresh_title()
 
336
        self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
 
337
            *self.noteset.get_category_property(cat, "bgcolor_hsv")),
 
338
            alpha=1))
 
339
        self.cbText.set_rgba(Gdk.RGBA(
 
340
            *self.noteset.get_category_property(cat, "textcolor"),
 
341
            alpha=1))
 
342
        fontname = self.noteset.get_category_property(cat, "font")
 
343
        if not fontname:
 
344
            # Get the system default font, if none is set
 
345
            fontname = \
 
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)
 
350
 
 
351
    def refresh_title(self, *args):
 
352
        """Updates the title of the category"""
 
353
        name = self.noteset.categories[self.cat].get("name",
 
354
                _("New Category"))
 
355
        if self.noteset.properties.get("default_cat", "") == self.cat:
 
356
            name += " (" + _("Default Category") + ")"
 
357
        self.lExp.set_text(name)
 
358
 
 
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()
 
367
        winConfirm.destroy()
 
368
        if confirm == Gtk.ResponseType.ACCEPT:
 
369
            self.settingsdialog.delete_category(self.cat)
 
370
 
 
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()
 
378
 
 
379
    def eName_changed(self, *args):
 
380
        """Update a category name"""
 
381
        self.noteset.categories[self.cat]["name"] = self.eName.get_text()
 
382
        self.refresh_title()
 
383
        for note in self.noteset.notes:
 
384
            note.gui.populate_menu()
 
385
 
 
386
    def update_bg(self, *args):
 
387
        """Action to update the background color"""
 
388
        try:
 
389
            rgba = self.cbBG.get_rgba()
 
390
        except TypeError:
 
391
            rgba = Gdk.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.
 
400
        load_global_css()
 
401
 
 
402
    def update_textcolor(self, *args):
 
403
        """Action to update the text color"""
 
404
        try:
 
405
            rgba = self.cbText.get_rgba()
 
406
        except TypeError:
 
407
            rgba = Gdk.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()
 
413
 
 
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()
 
420
 
 
421
class SettingsDialog:
 
422
    """Manages the GUI of the settings dialog"""
 
423
    def __init__(self, noteset):
 
424
        self.noteset = noteset
 
425
        self.categories = {}
 
426
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
 
427
            '..'))
 
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"]
 
433
        for w in widgets:
 
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()
 
439
 
 
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,
 
444
                False, False, 0)
 
445
 
 
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)
 
451
 
 
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()
 
461
 
 
462
    def refresh_category_titles(self):
 
463
        for cid, catsettings in self.categories.items():
 
464
            catsettings.refresh_title()