~jconti/recent-notifications/trunk

37 by Jason Conti
Reorganizing to make it easier to build a package.
1
"""
2
MessageWindow.py
3
by Jason Conti
4
February 19, 2010
5
6
Popup window for viewing notifications.
7
"""
8
9
import gnomeapplet
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
10
import gobject
37 by Jason Conti
Reorganizing to make it easier to build a package.
11
import gtk
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
12
import logging
37 by Jason Conti
Reorganizing to make it easier to build a package.
13
59 by Jason Conti
Think I finally found a decent layout for the messages in the treeview. Still need to fix the time format (I don't like a leading 0 in single digit hours), and the scrolling (should stay on top unless scrolled down manually).
14
from string import Template
15
93 by Jason Conti
Pulling the treeview code out of MessageWindow and putting it in MessageList.
16
from MessageList import MessageList
81 by Jason Conti
Adding support for gettext translations and a pointless en_US translation as an example. Using distutils-extra to automatically build the translations and the bonobo server. Replaced the build_servers target with update_prefix. This performs basically the same as build_servers except that it will do the substitution on any .in file, and it just writes the output file without the .in, instead of adding it to the data file install list.
17
from Translations import _
79 by Jason Conti
Selecting new messages when they arrive. The selection is cleared when the message window is closed. Removed icon column and packed the icon cell renderer into the message column.
18
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
19
logger = logging.getLogger("MessageWindow")
20
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
21
class Column(object):
22
  """Maps column names to column numbers."""
105 by Jason Conti
Replaced the new message selections with an unread column in the model. This column is marked False when the message window is closed. Currently just displays NEW next to the message summary if the message is unread, but may add more if I get any ideas. Updated the translation template and added a script to simplify updating the template.
23
  APP_NAME  = 0
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
24
  SEPARATOR = 1
105 by Jason Conti
Replaced the new message selections with an unread column in the model. This column is marked False when the message window is closed. Currently just displays NEW next to the message summary if the message is unread, but may add more if I get any ideas. Updated the translation template and added a script to simplify updating the template.
25
  COUNT     = 2
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
26
37 by Jason Conti
Reorganizing to make it easier to build a package.
27
class MessageWindow(gtk.Window):
28
  """Shows the log of notification messages."""
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
29
  def __init__(self, parent):
37 by Jason Conti
Reorganizing to make it easier to build a package.
30
    gtk.Window.__init__(self)
31
32
    self._parent = parent
33
157 by Jason Conti
Revert hide window when focus is lost.
34
    self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DOCK)
37 by Jason Conti
Reorganizing to make it easier to build a package.
35
    self.set_decorated(False)
36
    self.set_resizable(False)
156 by Jason Conti
Fixed a bunch of bugs with the new applet display code.
37
    self.set_property("skip-taskbar-hint", True)
37 by Jason Conti
Reorganizing to make it easier to build a package.
38
    self.stick()
81 by Jason Conti
Adding support for gettext translations and a pointless en_US translation as an example. Using distutils-extra to automatically build the translations and the bonobo server. Replaced the build_servers target with update_prefix. This performs basically the same as build_servers except that it will do the substitution on any .in file, and it just writes the output file without the .in, instead of adding it to the data file install list.
39
    self.set_title(_("Message Window"))
62 by Jason Conti
Added a frame around the message window.
40
    self.set_border_width(0)
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
41
    self.set_size_request(400, 400)
42
107 by Jason Conti
Adding the frame back to the message window, changed the shadow type.
43
    self._frame = gtk.Frame()
44
    self._frame.set_border_width(0)
45
    self._frame.set_shadow_type(gtk.SHADOW_OUT)
46
    self.add(self._frame)
47
60 by Jason Conti
Fixed the scrollbar and added alignment/padding to the cells.
48
    self._vadjust = gtk.Adjustment()
49
    self._vadjust.connect("changed", self.on_vadjust_changed)
50
86 by Jason Conti
Adding a statusbar to list the number of messages.
51
    self._vbox = gtk.VBox()
107 by Jason Conti
Adding the frame back to the message window, changed the shadow type.
52
    self._frame.add(self._vbox)
86 by Jason Conti
Adding a statusbar to list the number of messages.
53
60 by Jason Conti
Fixed the scrollbar and added alignment/padding to the cells.
54
    self._scroll = gtk.ScrolledWindow(vadjustment=self._vadjust)
62 by Jason Conti
Added a frame around the message window.
55
    self._scroll.set_border_width(5)
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
56
    self._scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
106 by Jason Conti
Adding code to adjust the wrap width of the message list based on the size of the scrolled window that contains it. Removed the frame from the message window. Adding a script to send test notifications.
57
    self._scroll.connect("size-allocate", self.on_scroll_resize)
86 by Jason Conti
Adding a statusbar to list the number of messages.
58
    self._vbox.pack_start(self._scroll, True)
59
148 by Jason Conti
Forgot to update the message counts as messages are removed.
60
    self._message_list = MessageList(self)
93 by Jason Conti
Pulling the treeview code out of MessageWindow and putting it in MessageList.
61
    self._scroll.add(self._message_list)
37 by Jason Conti
Reorganizing to make it easier to build a package.
62
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
63
    # app_name, is_separator, count
64
    self._combolist = gtk.ListStore(str, bool, int)
65
    self._combobox = gtk.ComboBox(self._combolist)
66
    self._combo_app_cell = gtk.CellRendererText()
67
    self._combobox.pack_start(self._combo_app_cell, True)
68
    self._combobox.add_attribute(self._combo_app_cell, "text", Column.APP_NAME)
69
    self._combo_count_cell = gtk.CellRendererText()
70
    self._combobox.pack_start(self._combo_count_cell, False)
71
    self._combobox.add_attribute(self._combo_count_cell, "text", Column.COUNT)
72
    self._combobox.set_row_separator_func(lambda m, i: m.get_value(i, Column.SEPARATOR))
73
    self._combobox.connect("changed", self.on_combobox_changed)
74
    self._vbox.pack_start(self._combobox, False)
75
76
    self.initialize_combobox()
77
78
  def on_combobox_changed(self, combobox, *args):
79
    """Set the tree filters to the selected app."""
80
    active = self._combobox.get_active()
104 by Jason Conti
Switch to TreeModelFilter broke selecting new messages. Updated the MessageList to select the row if it is currently visible in the filter. This was causing inconsistent behavior though. When the filter is changed, and the selected item is still visible, it will still be selected, which makes sense. But if an item that should be selected wasn't visible before the change and becomes visible, it won't be selected. To be consistent, either everything needs to be deselected when the filter is changed, or things that should be selected need to be selected (by perhaps adding a boolean row to the model). The first option is easier, so I'm adding that for now.
81
    self._message_list.unselect_all()
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
82
    if active == 0:
83
      self._message_list.set_filter_app(None)
84
    else:
85
      iter = self._combobox.get_active_iter()
86
      app_name = self._combolist.get_value(iter, Column.APP_NAME)
87
      self._message_list.set_filter_app(app_name)
88
106 by Jason Conti
Adding code to adjust the wrap width of the message list based on the size of the scrolled window that contains it. Removed the frame from the message window. Adding a script to send test notifications.
89
  def on_scroll_resize(self, scroll, allocation, *args):
90
    """Updates the message list wrap width when the scrolled window resizes."""
91
    self._message_list.set_wrap_width(allocation.width)
92
60 by Jason Conti
Fixed the scrollbar and added alignment/padding to the cells.
93
  def on_vadjust_changed(self, adjustment, *args):
94
    """Reset the adjustment position when the adjustment is changed (resized), but
95
    not moved, so the user can scroll around freely."""
96
    self._vadjust.set_value(0)
97
37 by Jason Conti
Reorganizing to make it easier to build a package.
98
  def add_message(self, message):
99
    """Adds a message to the message window."""
93 by Jason Conti
Pulling the treeview code out of MessageWindow and putting it in MessageList.
100
    self._message_list.add_message(message)
37 by Jason Conti
Reorganizing to make it easier to build a package.
101
75 by Jason Conti
Adding an option to the context menu to clear the message window. Adding more docstrings to Main.py.
102
  def clear_messages(self):
103
    """Clears all messages from the message window."""
93 by Jason Conti
Pulling the treeview code out of MessageWindow and putting it in MessageList.
104
    self._message_list.clear()
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
105
    self.initialize_combobox()
75 by Jason Conti
Adding an option to the context menu to clear the message window. Adding more docstrings to Main.py.
106
79 by Jason Conti
Selecting new messages when they arrive. The selection is cleared when the message window is closed. Removed icon column and packed the icon cell renderer into the message column.
107
  def clear_selections(self):
105 by Jason Conti
Replaced the new message selections with an unread column in the model. This column is marked False when the message window is closed. Currently just displays NEW next to the message summary if the message is unread, but may add more if I get any ideas. Updated the translation template and added a script to simplify updating the template.
108
    """Unselects every row in the treeview and marks the messages as read."""
109
    self._message_list.mark_as_read()
93 by Jason Conti
Pulling the treeview code out of MessageWindow and putting it in MessageList.
110
    self._message_list.unselect_all()
79 by Jason Conti
Selecting new messages when they arrive. The selection is cleared when the message window is closed. Removed icon column and packed the icon cell renderer into the message column.
111
153 by Jason Conti
Experiment with hiding the message window when it loses focus.
112
  def show_dropdown(self):
113
    """Shows the dropdown window"""
114
    self.show_all()
115
    self.align_with_parent()
116
117
  def hide_dropdown(self):
118
    """Hides the dropdown window"""
119
    self.hide_all()
120
    self.clear_selections()
121
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
122
  def increment_count(self, iter):
123
    """Increments the message count at the given row."""
124
    count = self._combolist.get_value(iter, Column.COUNT)
125
    self._combolist.set_value(iter, Column.COUNT, count + 1)
126
148 by Jason Conti
Forgot to update the message counts as messages are removed.
127
  def decrement_count(self, iter):
128
    """Decrements the message count at the given row."""
129
    count = self._combolist.get_value(iter, Column.COUNT)
130
    self._combolist.set_value(iter, Column.COUNT, count - 1)
131
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
132
  def initialize_combobox(self):
133
    """Clear the combobox and set the initial entry."""
134
    self._combolist.clear()
135
    self._combolist.append([_("All Applications"), False, 0])
136
    self._comboapps = {}
137
    self._combobox.set_active(0)
138
145 by Jason Conti
Implemented the message limit preference. This required a change to marking the messages as read. Previously, iters to unread items were saved, and then just those iters were marked as unread. I was always a bit wary of this, because I wasn't sure what would happen if I got a stale iter. Well, I found out. The app segfaults. This seems like a bug in GTK, but either way, I decided to stop saving iters. Under the assumption that only the first n messages will be unread, and there are no gaps (which should be correct), the app is now looping through the iters until it finds one that is already read (or runs out).
139
  def set_message_limit(self, n):
140
    """Sets the message limit of the MessageList."""
141
    self._message_list.set_message_limit(n)
142
148 by Jason Conti
Forgot to update the message counts as messages are removed.
143
  def increment_combobox(self, app_name):
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
144
    """Updates the combobox app names and message counts."""
145
    # Add a separator if there isn't one yet
146
    if len(self._combolist) == 1:
147
      self._combolist.append(["Separator", True, 0])
148
149
    self.increment_count(self._combolist.get_iter_first())
150
151
    if app_name not in self._comboapps:
152
      iter = self._combolist.append([app_name, False, 1])
153
      self._comboapps[app_name] = iter
86 by Jason Conti
Adding a statusbar to list the number of messages.
154
    else:
102 by Jason Conti
Replacing the statusbar with a combobox that shows the number of messages per application, as well as the total number of messages, and allows filtering messages based on app.
155
      self.increment_count(self._comboapps[app_name])
86 by Jason Conti
Adding a statusbar to list the number of messages.
156
148 by Jason Conti
Forgot to update the message counts as messages are removed.
157
  def decrement_combobox(self, app_name):
158
    """Updates the combobox app names and message counts."""
159
    self.decrement_count(self._combolist.get_iter_first())
160
161
    if app_name in self._comboapps:
162
      self.decrement_count(self._comboapps[app_name])
163
37 by Jason Conti
Reorganizing to make it easier to build a package.
164
  def align_with_parent(self):
165
    """Adapted from position_calendar_popup in the GNOME clock applet."""
166
    # Get the size of the applet button and the size of this window
150 by Jason Conti
Adding debug messages for align_with_parent to try to uncover strange positioning issues.
167
    applet_x, applet_y = x, y = self._parent.get_origin()
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
168
    w, h = self.get_size()
37 by Jason Conti
Reorganizing to make it easier to build a package.
169
    button_w, button_h = self._parent.get_size()
170
171
    screen = self.get_screen()
172
    monitor = None
150 by Jason Conti
Adding debug messages for align_with_parent to try to uncover strange positioning issues.
173
    monitor_index = -1
37 by Jason Conti
Reorganizing to make it easier to build a package.
174
175
    # Figure out which monitor we are on
176
    for i in range(screen.get_n_monitors()):
177
      m = screen.get_monitor_geometry(i)
178
179
      if x >= m.x and x <= m.x + m.width and y >= m.y and y <= m.y + m.height:
180
        monitor = m
150 by Jason Conti
Adding debug messages for align_with_parent to try to uncover strange positioning issues.
181
        monitor_index = i
37 by Jason Conti
Reorganizing to make it easier to build a package.
182
        break
183
184
    # Probably xinerama
185
    if monitor == None:
186
      monitor = gtk.gdk.Rectangle(0, 0, screen.get_width(), screen.get_height())
187
188
    orient = self._parent.get_orient()
189
190
    # Figure out the x, y coordinates of the window and the gravity depending
191
    # on the orientation of the panel
192
    if orient == gnomeapplet.ORIENT_RIGHT:  # Panel on left
193
      x += button_w
194
      if (y + h) > monitor.y + monitor.height:
195
        y -= (y + h) - (monitor.y + monitor.height)
196
197
    elif orient == gnomeapplet.ORIENT_LEFT: # Panel on right
198
      x -= w
199
      if (y + h) > monitor.y + monitor.height:
200
        y -= (y + h) - (monitor.y + monitor.height)
201
202
    elif orient == gnomeapplet.ORIENT_DOWN: # Panel on top
203
      y += button_h
204
      if (x + w) > monitor.x + monitor.width:
205
        x -= (x + w) - (monitor.x + monitor.width)
206
207
    else: # Panel on bottom
208
      y -= h
209
      if (x + w) > monitor.x + monitor.width:
210
        x -= (x + w) - (monitor.x + monitor.width)
211
165 by Jason Conti
It seems my broken window manager ignores gravity settings, so I never noticed that they were incorrect. This seems to result in incorrect positioning on people with working window managers (that don't ignore the gravity). Removed the gravity calculations from align_with_parent, and assuming everything is calculated from GRAVITY_NORTH_WEST. Still works here, hope it fixes the issue.
212
    gravity = gtk.gdk.GRAVITY_NORTH_WEST
37 by Jason Conti
Reorganizing to make it easier to build a package.
213
164 by Jason Conti
According to the pygtk docs, apparently the gravity should be set before move is called. Hoping this might fix the positioning bug. No idea.
214
    self.set_gravity(gravity)
37 by Jason Conti
Reorganizing to make it easier to build a package.
215
    self.move(x, y)
56 by Jason Conti
Working on a TreeView with a custom cellrenderer to display the notifications. Works, but still needs lots of work.
216
150 by Jason Conti
Adding debug messages for align_with_parent to try to uncover strange positioning issues.
217
    logger.debug("""
218
--------------------------------------------------
219
align_with_parent:
220
  applet:
221
    x: {0}
222
    y: {1}
223
    w: {2}
224
    h: {3}
225
    orient: {4}
226
  window:
227
    x: {5}
228
    y: {6}
229
    w: {7}
230
    h: {8}
231
    gravity: {9}
232
  monitor:
233
    x: {10}
234
    y: {11}
235
    w: {12}
236
    h: {13}
237
    index: {14}
238
--------------------------------------------------
239
    """.format(applet_x, applet_y, button_w, button_h, orient,
240
      x, y, w, h, gravity,
241
      monitor.x, monitor.y, monitor.width, monitor.height, monitor_index))
242