~umang/indicator-stickynotes/trunk

« back to all changes in this revision

Viewing changes to stickynotes/gui.py

  • Committer: Umang Varma
  • Date: 2022-03-12 23:42:25 UTC
  • Revision ID: git-v1:25d7d8ac55be70373a0664ad69cc5e186926d43b
Translation credits

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
2
 
# Copyright © 2012 Umang Varma <umang.me@gmail.com>
 
1
# Copyright © 2012-2018 Umang Varma <umang.me@gmail.com>
3
2
4
3
# This file is part of indicator-stickynotes.
5
4
17
16
# indicator-stickynotes.  If not, see <http://www.gnu.org/licenses/>.
18
17
 
19
18
from datetime import datetime
20
 
 
21
 
from gi.repository import Gtk, Gdk, Gio, GObject, GtkSource
 
19
from string import Template
 
20
import gi
 
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 _
22
25
import os.path
23
 
 
24
 
class StickyNote(object):
25
 
 
 
26
import colorsys
 
27
import uuid
 
28
 
 
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__), "..",
 
33
        "style_global.css"))
 
34
    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
 
35
            global_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
 
36
 
 
37
class StickyNote:
 
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__),
28
42
            '..'))
29
43
        self.note = note
 
44
        self.noteset = note.noteset
30
45
        self.locked = self.note.properties.get("locked", False)
 
46
 
 
47
        # Create menu
 
48
        self.menu = Gtk.Menu()
 
49
        self.populate_menu()
 
50
 
 
51
        # Load CSS template and initialize Gtk.CssProvider
 
52
        with open(os.path.join(self.path, "style.css"), encoding="utf-8") \
 
53
                as css_file:
 
54
            self.css_template = Template(css_file.read())
 
55
        self.css = Gtk.CssProvider()
 
56
 
 
57
        self.build_note()
 
58
        
 
59
    def build_note(self):
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,
34
 
            "StickyNotes.glade"))
 
63
            "StickyNotes.ui"))
35
64
        self.builder.connect_signals(self)
 
65
        self.winMain = self.builder.get_object("MainWindow")
 
66
 
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")
49
 
        # Run
50
 
        self.run()
51
 
 
52
 
    def run(self, *args):
53
 
        # (Maybe?) Remove this eventually
 
68
        widgets = ["txtNote", "bAdd", "imgAdd", "imgResizeR", "eResizeR",
 
69
                "bLock", "imgLock", "imgUnlock", "imgClose", "imgDropdown",
 
70
                "bClose", "confirmDelete", "movebox1", "movebox2"]
 
71
        for w in widgets:
 
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!
 
76
        self.update_style()
 
77
        self.update_font()
 
78
        # Ensure buttons are displayed with images
54
79
        settings = Gtk.Settings.get_default()
55
80
        settings.props.gtk_button_images = True
56
 
        # Load and set CSS
57
 
        css = Gtk.CssProvider()
58
 
        css.load_from_file(Gio.File.new_for_path(os.path.join(self.path,
59
 
            "style.css")))
60
 
        Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
61
 
                css, 800)
62
81
        # Set text buffer
63
82
        self.bbody = GtkSource.Buffer()
64
83
        self.bbody.begin_not_undoable_action()
66
85
        self.bbody.set_highlight_matching_brackets(False)
67
86
        self.bbody.end_not_undoable_action()
68
87
        self.txtNote.set_buffer(self.bbody)
69
 
        #Show
70
 
        self.winMain.show()
71
88
        # Make resize work
72
89
        self.winMain.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
73
90
        self.eResizeR.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
74
91
        # Move Window
75
92
        self.winMain.move(*self.note.properties.get("position", (10,10)))
76
93
        self.winMain.resize(*self.note.properties.get("size", (200,150)))
 
94
        # Show the window
 
95
        self.winMain.set_skip_pager_hint(True)
 
96
        self.winMain.show_all()
77
97
        # Mouse over
78
98
        self.eResizeR.get_window().set_cursor(Gdk.Cursor.new_for_display(
79
99
                    self.eResizeR.get_window().get_display(),
81
101
        # Set locked state
82
102
        self.set_locked_state(self.locked)
83
103
 
84
 
    def show(self, widget=None, event=None):
85
 
        self.winMain.present()
86
 
        self.winMain.stick()
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)
 
109
 
 
110
        # immediately undo the set keep above after the window
 
111
        # is shown, so that windows won't stay up if we switch to
 
112
        # a different window
 
113
        self.winMain.set_keep_above(False)
 
114
 
 
115
 
 
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)
 
121
 
 
122
    # workaround which is based on deleting a sticky note and re-initializing
 
123
    # it. 
 
124
    def show(self, widget=None, event=None, reload_from_backend=False):
 
125
        """Shows the stickynotes window"""
 
126
 
 
127
        # don't overwrite settings if loading from backend
 
128
        if not reload_from_backend:
 
129
            # store sticky note's settings
 
130
            self.update_note()
 
131
        else:
 
132
            # Categories may have changed in backend
 
133
            self.populate_menu()
 
134
 
 
135
        # destroy its main window
 
136
        self.winMain.destroy()
 
137
 
 
138
        # reinitialize that window
 
139
        self.build_note()
88
140
 
89
141
    def hide(self, *args):
 
142
        """Hides the stickynotes window"""
90
143
        self.winMain.hide()
91
144
 
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))
95
149
 
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())
99
154
        return False
100
155
 
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())
104
160
        return True
105
161
 
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))
112
169
        return prop
113
170
 
 
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)
 
178
 
 
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)
 
188
 
 
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"))
 
201
 
 
202
    def css_data(self):
 
203
        """Returns data to substitute into the CSS template"""
 
204
        data = {}
 
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))
 
207
            for a in x])
 
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"))
 
213
        return data
 
214
 
 
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)
 
220
 
 
221
        aot = Gtk.CheckMenuItem.new_with_label(_("Always on top"))
 
222
        aot.connect("toggled", self.malways_on_top_toggled)
 
223
        self.menu.append(aot)
 
224
        aot.show()
 
225
 
 
226
        mset = Gtk.MenuItem(_("Settings"))
 
227
        mset.connect("activate", self.noteset.indicator.show_settings)
 
228
        self.menu.append(mset)
 
229
        mset.show()
 
230
 
 
231
        sep = Gtk.SeparatorMenuItem()
 
232
        self.menu.append(sep)
 
233
        sep.show()
 
234
 
 
235
        catgroup = []
 
236
        mcats = Gtk.RadioMenuItem.new_with_label(catgroup,
 
237
                _("Categories:"))
 
238
        self.menu.append(mcats)
 
239
        mcats.set_sensitive(False)
 
240
        catgroup = mcats.get_group()
 
241
        mcats.show()
 
242
 
 
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)
 
251
            mitem.show()
 
252
 
 
253
    def malways_on_top_toggled(self, widget, *args):
 
254
        self.winMain.set_keep_above(widget.get_active())
 
255
 
114
256
    def save(self, *args):
115
257
        self.note.noteset.save()
116
258
        return False
117
259
 
118
260
    def add(self, *args):
119
 
        self.note.noteset.new()
 
261
        new_note = self.note.noteset.new()
 
262
 
 
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
 
266
 
 
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)
 
271
 
120
272
        return False
121
273
 
122
274
    def delete(self, *args):
123
 
        confirm = self.confirmDelete.run()
124
 
        self.confirmDelete.hide()
125
 
        if confirm == 1:
 
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()
 
281
        winConfirm.destroy()
 
282
        if confirm == Gtk.ResponseType.ACCEPT:
126
283
            self.note.delete()
127
 
            self.winMain.hide()
 
284
            self.winMain.destroy()
128
285
            return False
129
286
        else:
130
287
            return True
131
288
 
 
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())
 
293
 
 
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
 
299
        self.update_style()
 
300
        self.update_font()
 
301
 
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])
140
 
 
141
 
    def lock_toggled(self, *args):
142
 
        self.set_locked_state(self.bLock.get_active())
143
 
 
144
 
    def quit(self, *args):
145
 
        Gtk.main_quit()
 
309
        self.bLock.set_tooltip_text({True: _("Unlock"),
 
310
            False: _("Lock")}[self.locked])
 
311
 
 
312
    def lock_clicked(self, *args):
 
313
        """Toggle the locked state of the note"""
 
314
        self.set_locked_state(not self.locked)
146
315
 
147
316
    def focus_out(self, *args):
148
317
        self.save(*args)
149
318
 
 
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")
 
325
    ret =  winAbout.run()
 
326
    winAbout.destroy()
 
327
    return ret
 
328
 
 
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
 
334
        self.cat = cat
 
335
        self.builder = Gtk.Builder()
 
336
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
 
337
            '..'))
 
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"]
 
343
        for w in widgets:
 
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)
 
347
        self.refresh_title()
 
348
        self.cbBG.set_rgba(Gdk.RGBA(*colorsys.hsv_to_rgb(
 
349
            *self.noteset.get_category_property(cat, "bgcolor_hsv")),
 
350
            alpha=1))
 
351
        self.cbText.set_rgba(Gdk.RGBA(
 
352
            *self.noteset.get_category_property(cat, "textcolor"),
 
353
            alpha=1))
 
354
        fontname = self.noteset.get_category_property(cat, "font")
 
355
        if not fontname:
 
356
            # Get the system default font, if none is set
 
357
            fontname = \
 
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)
 
362
 
 
363
    def refresh_title(self, *args):
 
364
        """Updates the title of the category"""
 
365
        name = self.noteset.categories[self.cat].get("name",
 
366
                _("New Category"))
 
367
        if self.noteset.properties.get("default_cat", "") == self.cat:
 
368
            name += " (" + _("Default Category") + ")"
 
369
        self.lExp.set_text(name)
 
370
 
 
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()
 
379
        winConfirm.destroy()
 
380
        if confirm == Gtk.ResponseType.ACCEPT:
 
381
            self.settingsdialog.delete_category(self.cat)
 
382
 
 
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()
 
390
 
 
391
    def eName_changed(self, *args):
 
392
        """Update a category name"""
 
393
        self.noteset.categories[self.cat]["name"] = self.eName.get_text()
 
394
        self.refresh_title()
 
395
        for note in self.noteset.notes:
 
396
            note.gui.populate_menu()
 
397
 
 
398
    def update_bg(self, *args):
 
399
        """Action to update the background color"""
 
400
        try:
 
401
            rgba = self.cbBG.get_rgba()
 
402
        except TypeError:
 
403
            rgba = Gdk.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.
 
412
        load_global_css()
 
413
 
 
414
    def update_textcolor(self, *args):
 
415
        """Action to update the text color"""
 
416
        try:
 
417
            rgba = self.cbText.get_rgba()
 
418
        except TypeError:
 
419
            rgba = Gdk.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()
 
425
 
 
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()
 
432
 
 
433
class SettingsDialog:
 
434
    """Manages the GUI of the settings dialog"""
 
435
    def __init__(self, noteset):
 
436
        self.noteset = noteset
 
437
        self.categories = {}
 
438
        self.path = os.path.abspath(os.path.join(os.path.dirname(__file__),
 
439
            '..'))
 
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"]
 
445
        for w in widgets:
 
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()
 
451
 
 
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,
 
456
                False, False, 0)
 
457
 
 
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)
 
463
 
 
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()
 
473
 
 
474
    def refresh_category_titles(self):
 
475
        for cid, catsettings in self.categories.items():
 
476
            catsettings.refresh_title()