~umang/indicator-stickynotes/trunk

« back to all changes in this revision

Viewing changes to stickynotes/gui.py

  • Committer: Umang Varma
  • Date: 2012-06-05 05:51:49 UTC
  • Revision ID: git-v1:642b6a22fb7dae75d6a22bf6d8a874794e4fbd7c
Hide all shouldn't mess up position/size

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("Icons/" + filename + suffix +
197
 
                    ".png")
198
 
 
199
 
    def css_data(self):
200
 
        """Returns data to substitute into the CSS template"""
201
 
        data = {}
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))
204
 
            for a in x])
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"))
210
 
        return data
211
 
 
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)
217
 
 
218
 
        aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
219
 
        aot.connect("toggled", self.malways_on_top_toggled)
220
 
        self.menu.append(aot)
221
 
        aot.show()
222
 
 
223
 
        mset = Gtk.MenuItem(_("Settings"))
224
 
        mset.connect("activate", self.noteset.indicator.show_settings)
225
 
        self.menu.append(mset)
226
 
        mset.show()
227
 
 
228
 
        sep = Gtk.SeparatorMenuItem()
229
 
        self.menu.append(sep)
230
 
        sep.show()
231
 
 
232
 
        catgroup = []
233
 
        mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
234
 
                _("Categories:"))
235
 
        self.menu.append(mcats)
236
 
        mcats.set_sensitive(False)
237
 
        catgroup = mcats.get_group()
238
 
        mcats.show()
239
 
 
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)
248
 
            mitem.show()
249
 
 
250
 
    def malways_on_top_toggled(self, widget, *args):
251
 
        self.winMain.set_keep_above(widget.get_active())
252
 
 
253
 
    def save(self, *args):
254
 
        self.note.noteset.save()
255
 
        return False
256
 
 
257
 
    def add(self, *args):
258
 
        self.note.noteset.new()
259
 
        return False
260
 
 
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()
268
 
        winConfirm.destroy()
269
 
        if confirm == Gtk.ResponseType.ACCEPT:
270
 
            self.note.delete()
271
 
            self.winMain.destroy()
272
 
            return False
273
 
        else:
274
 
            return True
275
 
 
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())
280
 
 
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
286
 
        self.update_style()
287
 
        self.update_font()
288
 
 
289
 
    def set_locked_state(self, locked):
290
 
        """Change the locked state of the stickynote"""
291
 
        self.locked = locked
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])
298
 
 
299
 
    def lock_clicked(self, *args):
300
 
        """Toggle the locked state of the note"""
301
 
        self.set_locked_state(not self.locked)
302
 
 
303
 
    def focus_out(self, *args):
304
 
        self.save(*args)
305
 
 
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")
312
 
    ret =  winAbout.run()
313
 
    winAbout.destroy()
314
 
    return ret
315
 
 
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
321
 
        self.cat = cat
322
 
        self.builder = Gtk.Builder()
323
 
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
324
 
            '..'))
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"]
330
 
        for w in widgets:
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)
334
 
        self.refresh_title()
335
 
        self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
336
 
            *self.noteset.get_category_property(cat, "bgcolor_hsv")),
337
 
            alpha=1))
338
 
        self.cbText.set_rgba(Gdk.RGBA(
339
 
            *self.noteset.get_category_property(cat, "textcolor"),
340
 
            alpha=1))
341
 
        fontname = self.noteset.get_category_property(cat, "font")
342
 
        if not fontname:
343
 
            # Get the system default font, if none is set
344
 
            fontname = \
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)
349
 
 
350
 
    def refresh_title(self, *args):
351
 
        """Updates the title of the category"""
352
 
        name = self.noteset.categories[self.cat].get("name",
353
 
                _("New Category"))
354
 
        if self.noteset.properties.get("default_cat", "") == self.cat:
355
 
            name += " (" + _("Default Category") + ")"
356
 
        self.lExp.set_text(name)
357
 
 
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()
366
 
        winConfirm.destroy()
367
 
        if confirm == Gtk.ResponseType.ACCEPT:
368
 
            self.settingsdialog.delete_category(self.cat)
369
 
 
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()
377
 
 
378
 
    def eName_changed(self, *args):
379
 
        """Update a category name"""
380
 
        self.noteset.categories[self.cat]["name"] = self.eName.get_text()
381
 
        self.refresh_title()
382
 
        for note in self.noteset.notes:
383
 
            note.gui.populate_menu()
384
 
 
385
 
    def update_bg(self, *args):
386
 
        """Action to update the background color"""
387
 
        try:
388
 
            rgba = self.cbBG.get_rgba()
389
 
        except TypeError:
390
 
            rgba = Gdk.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.
399
 
        load_global_css()
400
 
 
401
 
    def update_textcolor(self, *args):
402
 
        """Action to update the text color"""
403
 
        try:
404
 
            rgba = self.cbText.get_rgba()
405
 
        except TypeError:
406
 
            rgba = Gdk.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()
412
 
 
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()
419
 
 
420
 
class SettingsDialog:
421
 
    """Manages the GUI of the settings dialog"""
422
 
    def __init__(self, noteset):
423
 
        self.noteset = noteset
424
 
        self.categories = {}
425
 
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
426
 
            '..'))
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"]
432
 
        for w in widgets:
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()
438
 
 
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,
443
 
                False, False, 0)
444
 
 
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)
450
 
 
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()
460
 
 
461
 
    def refresh_category_titles(self):
462
 
        for cid, catsettings in self.categories.items():
463
 
            catsettings.refresh_title()