1/*
2 *  Notes - panel plugin for Xfce Desktop Environment
3 *  Copyright (c) 2009-2010  Mike Massonnet <mmassonnet@xfce.org>
4 *
5 *  TODO:
6 *  - Follow GNOME bug #551184 to change accelerators hexa values
7 *
8 *  This program is free software; you can redistribute it and/or modify
9 *  it under the terms of the GNU General Public License as published by
10 *  the Free Software Foundation; either version 2 of the License, or
11 *  (at your option) any later version.
12 *
13 *  This program is distributed in the hope that it will be useful,
14 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *  GNU Library General Public License for more details.
17 *
18 *  You should have received a copy of the GNU General Public License
19 *  along with this program; if not, write to the Free Software
20 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
21 */
22
23using Gtk;
24using Pango;
25
26namespace Xnp {
27
28	public class Window : Gtk.Window {
29
30		private int width;
31		private int height;
32		private Gtk.Menu menu;
33		private Gtk.CheckMenuItem mi_above;
34		private Gtk.CheckMenuItem mi_sticky;
35		private Gtk.Image menu_image;
36		private Gdk.Pixbuf menu_pixbuf;
37		private Gdk.Pixbuf menu_hover_pixbuf;
38		private Gtk.Label title_label;
39		private Xnp.TitleBarButton refresh_button;
40		private Xnp.TitleBarButton left_arrow_button;
41		private Xnp.TitleBarButton right_arrow_button;
42		private Xnp.TitleBarButton close_button;
43		private Gtk.Box content_box;
44		private Gtk.Notebook notebook;
45
46		private Gtk.UIManager ui;
47		private const string ui_string =
48"""
49<ui>
50  <accelerator action="close-window" />
51  <accelerator action="new-window" />
52  <accelerator action="delete-window" />
53  <accelerator action="rename-window" />
54  <accelerator action="new-note" />
55  <accelerator action="delete-note" />
56  <accelerator action="rename-note" />
57  <accelerator action="cancel" />
58  <accelerator action="next-note" />
59  <accelerator action="prev-note" />
60</ui>
61""";
62		private Gtk.ActionGroup action_group;
63		private const Gtk.ActionEntry[] action_entries = {
64			{ "close-window",  null, null, "Escape", null, hide },
65			{ "new-window",    null, null, "<Ctrl><Shift>n", null, action_new_window },
66			{ "delete-window", null, null, "<Ctrl><Shift>w", null, action_delete_window },
67			{ "rename-window", null, null, "<Shift>F2", null, action_rename_window },
68			{ "new-note",      null, null, "<Ctrl>n", null, action_new_note },
69			{ "delete-note",   null, null, "<Ctrl>w", null, action_delete_note },
70			{ "rename-note",   null, null, "F2", null, action_rename_note },
71			{ "cancel",        null, null, "<Ctrl>z", null, action_cancel },
72			{ "next-note",     null, null, "<Ctrl>Page_Down", null, action_next_note },
73			{ "prev-note",     null, null, "<Ctrl>Page_Up", null, action_prev_note }
74		};
75
76		private int CORNER_MARGIN = 20;
77		private Gdk.Cursor CURSOR_RIGHT = new Gdk.Cursor.for_display (Gdk.Display.get_default(), Gdk.CursorType.RIGHT_SIDE);
78		private Gdk.Cursor CURSOR_LEFT = new Gdk.Cursor.for_display (Gdk.Display.get_default(), Gdk.CursorType.LEFT_SIDE);
79		private Gdk.Cursor CURSOR_BOTTOM_RC = new Gdk.Cursor.for_display (Gdk.Display.get_default(), Gdk.CursorType.BOTTOM_RIGHT_CORNER);
80		private Gdk.Cursor CURSOR_BOTTOM = new Gdk.Cursor.for_display (Gdk.Display.get_default(), Gdk.CursorType.BOTTOM_SIDE);
81		private Gdk.Cursor CURSOR_BOTTOM_LC = new Gdk.Cursor.for_display (Gdk.Display.get_default(), Gdk.CursorType.BOTTOM_LEFT_CORNER);
82
83		private unowned SList<Xnp.Window> window_list;
84
85		public new string name { default = _("Notes"); get; set; }
86		public int n_pages { get; set; }
87
88		public bool show_tabs {
89			get {
90				return this.notebook.show_tabs;
91			}
92			set {
93				this.notebook.show_tabs = value;
94			}
95		}
96
97		private int _tabs_position;
98		public int tabs_position {
99			get {
100				return this._tabs_position;
101			}
102			set {
103				this._tabs_position = value;
104				if (this._tabs_position == 0)
105					show_tabs = false;
106				else {
107					show_tabs = true;
108					_notebook_update_tabs_angle ();
109					if (this._tabs_position == 1)
110						this.notebook.tab_pos = Gtk.PositionType.TOP;
111					else if (this._tabs_position == 2)
112						this.notebook.tab_pos = Gtk.PositionType.RIGHT;
113					else if (this._tabs_position == 3)
114						this.notebook.tab_pos = Gtk.PositionType.BOTTOM;
115					else if (this._tabs_position == 4)
116						this.notebook.tab_pos = Gtk.PositionType.LEFT;
117					else {
118						this.show_tabs = false;
119						warning ("Bad value for tabs-position");
120					}
121				}
122			}
123		}
124
125		private bool _above;
126		public bool above {
127			get {
128				return this._above;
129			}
130			set {
131				this._above = value;
132				set_keep_above (value);
133			}
134		}
135
136		private bool _sticky;
137		public bool sticky {
138			get {
139				return this._sticky;
140			}
141			set {
142				this._sticky = value;
143				if (value == true)
144					stick ();
145				else
146					unstick ();
147				if (this.mi_sticky is Gtk.CheckMenuItem)
148					this.mi_sticky.active = this._sticky;
149			}
150		}
151
152		private bool _show_refresh_button;
153		public bool show_refresh_button {
154			get {
155				return this._show_refresh_button;
156			}
157			set {
158				this._show_refresh_button = value;
159				if (value == true) {
160					this.refresh_button.show ();
161				}
162				else {
163					this.refresh_button.hide ();
164				}
165			}
166		}
167
168		public signal void action (string action);
169		public signal void save_data (Xnp.Note note);
170		public signal void note_inserted (Xnp.Note note);
171		public signal void note_deleted (Xnp.Note note);
172		public signal void note_renamed (Xnp.Note note, string old_name);
173
174		construct {
175			((Gtk.Widget)this).name = "notes-window";
176			this.title = _("Notes");
177			this.deletable = false;
178			this.skip_taskbar_hint = true;
179			this.default_height = 380;
180			this.default_width = 300;
181			this.decorated = false;
182			this.icon_name = "xfce4-notes-plugin";
183			this.sticky = true;
184			this.opacity = 0.9;
185		}
186
187		public Window () {
188			/* Window responses on pointer motion */
189			add_events (Gdk.EventMask.POINTER_MOTION_MASK|Gdk.EventMask.POINTER_MOTION_HINT_MASK|Gdk.EventMask.BUTTON_PRESS_MASK);
190
191			/* Build accelerators */
192			this.action_group = new Gtk.ActionGroup ("XNP");
193			this.action_group.add_actions (action_entries, this);
194
195			this.ui = new Gtk.UIManager ();
196			this.ui.insert_action_group (this.action_group, 0);
197			try {
198				this.ui.add_ui_from_string (this.ui_string , -1);
199				add_accel_group (this.ui.get_accel_group ());
200			}
201			catch (Error e) {
202				warning ("%s", e.message);
203			}
204
205			/* Build Menu */
206			this.menu = build_menu ();
207			this.menu.show_all ();
208
209			/* Build Frame */
210			var frame = new Gtk.Frame (null);
211			frame.shadow_type = Gtk.ShadowType.NONE;
212			var style = frame.get_modifier_style ();
213			style.xthickness = 1;
214			style.ythickness = 3;
215			frame.modify_style (style);
216			frame.show ();
217			add (frame);
218			var vbox_frame = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
219
220			vbox_frame.spacing = 1;
221			vbox_frame.show ();
222			frame.add (vbox_frame);
223
224			/* Build title bar */
225			var title_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
226			var menu_evbox = new Gtk.EventBox ();
227			menu_evbox.tooltip_text = _("Menu");
228			menu_evbox.set_visible_window (false);
229			try {
230				this.menu_pixbuf = new Gdk.Pixbuf.from_file ("%s/pixmaps/notes-menu.png".printf (Config.PKGDATADIR));
231				this.menu_hover_pixbuf = new Gdk.Pixbuf.from_file ("%s/pixmaps/notes-menu-active.png".printf (Config.PKGDATADIR));
232			}
233			catch (Error e) {
234				this.menu_pixbuf = this.menu_hover_pixbuf = null;
235			}
236			this.menu_image = new Gtk.Image.from_pixbuf (this.menu_pixbuf);
237			menu_evbox.add (this.menu_image);
238			menu_evbox.enter_notify_event.connect (() => {
239				this.menu_image.set_from_pixbuf (this.menu_hover_pixbuf);
240				return false;
241			});
242			menu_evbox.leave_notify_event.connect (() => {
243				this.menu_image.set_from_pixbuf (this.menu_pixbuf);
244				return false;
245			});
246			title_box.pack_start (menu_evbox, false, false, 2);
247			var title_evbox = new Gtk.EventBox ();
248			title_evbox.set_visible_window (false);
249			this.title_label = new Gtk.Label (null);
250			this.title_label.set_markup ("<b>"+this.title+"</b>");
251			this.title_label.ellipsize = Pango.EllipsizeMode.END;
252			this.title_label.xalign = (float)0.0;
253			title_evbox.add (this.title_label);
254			title_box.pack_start (title_evbox, true, true, 6);
255			this.refresh_button = new Xnp.TitleBarButton (Xnp.TitleBarButtonType.REFRESH);
256			this.refresh_button.tooltip_text = _("Refresh notes");
257			this.refresh_button.no_show_all = true;
258			this.refresh_button.sensitive = false;
259			title_box.pack_start (this.refresh_button, false, false, 2);
260			this.left_arrow_button = new Xnp.TitleBarButton (Xnp.TitleBarButtonType.LEFT_ARROW);
261			this.left_arrow_button.tooltip_text = Gtk.accelerator_get_label (0xff55, Gdk.ModifierType.CONTROL_MASK); // GDK_Page_Up
262			this.left_arrow_button.sensitive = false;
263			title_box.pack_start (this.left_arrow_button, false, false, 2);
264			this.right_arrow_button = new Xnp.TitleBarButton (Xnp.TitleBarButtonType.RIGHT_ARROW);
265			this.right_arrow_button.tooltip_text = Gtk.accelerator_get_label (0xff56, Gdk.ModifierType.CONTROL_MASK); // GDK_Page_Down
266			this.right_arrow_button.sensitive = false;
267			title_box.pack_start (this.right_arrow_button, false, false, 2);
268			this.close_button = new Xnp.TitleBarButton (Xnp.TitleBarButtonType.CLOSE);
269			this.close_button.tooltip_text = _("Hide (%s)").printf (Gtk.accelerator_get_label (0xff1b, 0)); // GDK_Escape
270			title_box.pack_start (this.close_button, false, false, 2);
271			title_box.show_all ();
272			vbox_frame.pack_start (title_box, false, false, 0);
273
274			/* Build content box */
275			this.content_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
276			this.content_box.show ();
277			vbox_frame.pack_start (this.content_box, true, true, 0);
278
279			/* Build Notebook */
280			this.notebook = new Gtk.Notebook ();
281			this.notebook.name = "notes-notebook";
282			this.notebook.show_border = true;
283			this.notebook.show_tabs = false;
284			this.notebook.tab_pos = Gtk.PositionType.TOP;
285			this.notebook.scrollable = true;
286			this.notebook.show ();
287			this.content_box.pack_start (this.notebook, true, true, 0);
288
289			/* Connect mouse click signals */
290			menu_evbox.button_press_event.connect (menu_evbox_pressed_cb);
291			this.refresh_button.clicked.connect (action_refresh_notes);
292			this.left_arrow_button.clicked.connect (action_prev_note);
293			this.right_arrow_button.clicked.connect (action_next_note);
294			this.close_button.clicked.connect (() => { hide (); });
295
296			/* Connect extra signals */
297			delete_event.connect (() => {
298				/* Replace ALT+F4 action */
299				hide ();
300				return true;
301			});
302			focus_in_event.connect (() => {
303				menu_image.sensitive = true;
304				title_label.sensitive = true;
305				refresh_button.sensitive = true;
306				update_navigation_sensitivity (this.notebook.get_current_page ());
307				close_button.sensitive = true;
308				return false;
309			});
310			focus_out_event.connect (() => {
311				menu_image.sensitive = false;
312				title_label.sensitive = false;
313				refresh_button.sensitive = false;
314				left_arrow_button.sensitive = false;
315				right_arrow_button.sensitive = false;
316				close_button.sensitive = false;
317				return false;
318			});
319			leave_notify_event.connect (window_leaved_cb);
320			motion_notify_event.connect (window_motion_cb);
321			button_press_event.connect (window_pressed_cb);
322			window_state_event.connect (window_state_cb);
323			title_evbox.button_press_event.connect (title_evbox_pressed_cb);
324			title_evbox.scroll_event.connect (title_evbox_scrolled_cb);
325			this.notebook.page_added.connect ((n, c, p) => {
326				notebook.set_current_page ((int)p);
327				update_navigation_sensitivity ((int)p);
328			});
329			this.notebook.page_removed.connect ((n, c, p) => {
330				update_navigation_sensitivity ((int)p);
331			});
332			this.notebook.switch_page.connect ((n, c, p) => {
333				var note = (Xnp.Note)(notebook.get_nth_page ((int)p));
334				update_title (note.name);
335				update_navigation_sensitivity ((int)p);
336			});
337			notify["name"].connect (() => {
338				int page = this.notebook.get_current_page ();
339				if (page == -1)
340					return;
341				var current_note = (Xnp.Note)(this.notebook.get_nth_page (page));
342				update_title (current_note.name);
343			});
344			notify["title"].connect (() => {
345				title_label.set_markup ("<b>"+title+"</b>");
346			});
347		}
348
349		~Window () {
350		}
351
352		/*
353		 * Signal callbacks
354		 */
355
356		/**
357		 * hide:
358		 *
359		 * Save position before hidding.
360		 */
361		public new void hide () {
362			int winx, winy;
363			get_position (out winx, out winy);
364			base.hide ();
365			deiconify ();
366			unshade ();
367			move (winx, winy);
368			set_keep_above (this.above);
369		}
370
371		/**
372		 * window_leaved_cb:
373		 *
374		 * Reset the mouse cursor.
375		 */
376		private bool window_leaved_cb () {
377			get_window ().set_cursor (null);
378			return true;
379		}
380
381		/**
382		 * window_motion_cb:
383		 *
384		 * Update mouse cursor.
385		 */
386		private bool window_motion_cb (Gdk.EventMotion event) {
387			Gtk.Allocation allocation;
388
389			get_allocation (out allocation);
390
391			if (event.x > 4 && event.y > 4
392				&& event.x < allocation.width - 4
393				&& event.y < allocation.height - 4) {
394				get_window ().set_cursor (null);
395				return false;
396			}
397
398			// Right
399			if (event.x >= allocation.width - this.CORNER_MARGIN
400				&& event.y >= this.CORNER_MARGIN
401				&& event.y < allocation.height - this.CORNER_MARGIN)
402				get_window ().set_cursor (this.CURSOR_RIGHT);
403			// Bottom right corner
404			else if (event.x >= allocation.width - this.CORNER_MARGIN
405				&& event.y >= allocation.height - this.CORNER_MARGIN)
406				get_window ().set_cursor (this.CURSOR_BOTTOM_RC);
407			// Bottom
408			else if (event.x > this.CORNER_MARGIN
409				&& event.y > allocation.height - this.CORNER_MARGIN
410				&& event.x < allocation.width - this.CORNER_MARGIN)
411				get_window ().set_cursor (this.CURSOR_BOTTOM);
412			// Bottom left corner
413			else if (event.x <= this.CORNER_MARGIN
414				&& event.y >= allocation.height - this.CORNER_MARGIN)
415				get_window ().set_cursor (this.CURSOR_BOTTOM_LC);
416			// Left
417			else if (event.x <= this.CORNER_MARGIN && event.y >= this.CORNER_MARGIN
418				&& event.y < allocation.height - this.CORNER_MARGIN)
419				get_window ().set_cursor (this.CURSOR_LEFT);
420			// Default
421			else
422				get_window ().set_cursor (null);
423
424			return true;
425		}
426
427		/**
428		 * window_pressed_cb:
429		 *
430		 * Start a window resize depending on mouse pointer location.
431		 */
432		private bool window_pressed_cb (Gdk.EventButton event) {
433			Gdk.WindowEdge edge;
434			Gtk.Allocation allocation;
435
436			get_allocation (out allocation);
437
438			if (event.x > 4 && event.y > 4
439				&& event.x < allocation.width - 4
440				&& event.y < allocation.height - 4)
441				return false;
442
443			// Right
444			if (event.y > this.CORNER_MARGIN
445				&& event.x > allocation.width - this.CORNER_MARGIN
446				&& event.y < allocation.height - this.CORNER_MARGIN)
447				edge = Gdk.WindowEdge.EAST;
448			// Bottom right corner
449			else if (event.x >= allocation.width - this.CORNER_MARGIN
450				&& event.y >= allocation.height - this.CORNER_MARGIN)
451				edge = Gdk.WindowEdge.SOUTH_EAST;
452			// Bottom
453			else if (event.x > this.CORNER_MARGIN
454				&& event.y > allocation.height - this.CORNER_MARGIN
455				&& event.x < allocation.width - this.CORNER_MARGIN)
456				edge = Gdk.WindowEdge.SOUTH;
457			// Bottom left corner
458			else if (event.x <= this.CORNER_MARGIN
459				&& event.y >= allocation.height - this.CORNER_MARGIN)
460				edge = Gdk.WindowEdge.SOUTH_WEST;
461			// Left
462			else if (event.y > this.CORNER_MARGIN && event.x < this.CORNER_MARGIN
463				&& event.y < allocation.height - this.CORNER_MARGIN)
464				edge = Gdk.WindowEdge.WEST;
465			else
466				return false;
467
468			begin_resize_drag (edge, (int)event.button,
469				(int)event.x_root, (int)event.y_root, event.time);
470
471			return true;
472		}
473
474		/**
475		 * window_state_cb:
476		 *
477		 * Watch window manager actions always on top and sticky
478		 * window.
479		 */
480		private bool window_state_cb (Gdk.EventWindowState event) {
481			if ((bool)(event.changed_mask & Gdk.WindowState.ABOVE)) {
482				/* FIXME above state is never notified despit
483				 * of xfwm4 switching the state */
484				this.mi_above.active = (bool)(event.new_window_state & Gdk.WindowState.ABOVE);
485			}
486			if ((bool)(event.changed_mask & Gdk.WindowState.STICKY) && get_visible ()) {
487				this.sticky = (bool)((event.new_window_state & Gdk.WindowState.STICKY) != 0);
488			}
489			return false;
490		}
491
492		/**
493		 * title_evbox_pressed_cb:
494		 *
495		 * Raise/lower the window and popup window menu.
496		 */
497		private bool title_evbox_pressed_cb (Gtk.Widget widget, Gdk.EventButton event) {
498			if (event.type != Gdk.EventType.BUTTON_PRESS)
499				return false;
500			if (event.button == 1) {
501				get_window ().show ();
502				int winx, winy, curx, cury;
503				get_position (out winx, out winy);
504				get_pointer (out curx, out cury);
505				winx += curx;
506				winy += cury;
507				begin_move_drag (1, winx, winy, Gtk.get_current_event_time ());
508			}
509			else if (event.button == 2) {
510				get_window ().lower ();
511			}
512			else if (event.button == 3) {
513				this.menu.popup (null, null, null, 0, Gtk.get_current_event_time ());
514			}
515			return false;
516		}
517
518		/**
519		 * title_evbox_scrolled_cb:
520		 *
521		 * Shade/unshade the window and set transparency by holding ALT.
522		 */
523		private bool title_evbox_scrolled_cb (Gtk.Widget widget, Gdk.EventScroll event) {
524			if ((bool)(event.state & Gdk.ModifierType.MOD1_MASK)) {
525				if (event.direction == Gdk.ScrollDirection.UP) {
526					opacity += 0.1;
527				} else if (event.direction == Gdk.ScrollDirection.DOWN) {
528					if (opacity - 0.1 >= 0.1)
529						opacity -= 0.1;
530				}
531			}
532			else {
533				if (event.direction == Gdk.ScrollDirection.UP) {
534					shade ();
535				}
536				else if (event.direction == Gdk.ScrollDirection.DOWN) {
537					unshade ();
538				}
539			}
540			return false;
541		}
542
543		/**
544		 * note_notify_name_cb:
545		 *
546		 */
547		private void note_notify_name_cb (GLib.Object object, GLib.ParamSpec pspec) {
548			Xnp.Note note = object as Xnp.Note;
549			this.notebook.set_tab_label_text (note, note.name);
550			_notebook_update_tabs_angle ();
551			int page = this.notebook.get_current_page ();
552			var current_note = (Xnp.Note)(this.notebook.get_nth_page (page));
553			if (note == current_note)
554				this.update_title (note.name);
555		}
556
557		/*
558		 * Action callbacks
559		 */
560
561		private void action_new_window () {
562			action ("create-new-window");
563		}
564
565		private void action_delete_window () {
566			action ("delete");
567		}
568
569		private void action_rename_window () {
570			action ("rename");
571		}
572
573		private void action_new_note () {
574			insert_note ();
575		}
576
577		private void action_delete_note () {
578			delete_current_note ();
579		}
580
581		private void action_rename_note () {
582			rename_current_note ();
583		}
584
585		private void action_cancel () {
586			int page = notebook.get_current_page ();
587			if (page < 0)
588				return;
589			Gtk.Widget child = notebook.get_nth_page (page);
590			((Xnp.Note)child).text_view.undo ();
591		}
592
593		private void action_refresh_notes () {
594			action ("refresh-notes");
595		}
596
597		private void action_next_note () {
598			notebook.next_page ();
599		}
600
601		private void action_prev_note () {
602			notebook.prev_page ();
603		}
604
605		/*
606		 * Window menu
607		 */
608
609		/**
610		 * menu_evbox_pressed_cb:
611		 *
612		 * Popup the window menu.
613		 */
614		private bool menu_evbox_pressed_cb (Gtk.Widget widget, Gdk.EventButton event) {
615			this.menu.popup (null, null, menu_position, 0, Gtk.get_current_event_time ());
616			return false;
617		}
618
619		/**
620		 * menu_position:
621		 *
622		 * Menu position function for the window menu.
623		 */
624		private void menu_position (Gtk.Menu menu, out int x, out int y, out bool push_in) {
625			int winx, winy, width, height;
626			Gtk.Requisition requisition;
627			Gtk.Allocation allocation;
628
629			get_window ().get_geometry (out winx, out winy, out width, out height);
630			menu.get_preferred_size (out requisition, null);
631			get_window ().get_origin (out x, out y);
632			push_in = false;
633
634			content_box.get_allocation (out allocation);
635
636			if (y + allocation.y + requisition.height > Gdk.Screen.height ()) {
637				/* Show menu above */
638				y -= requisition.height;
639			}
640			else {
641				/* Show menu below */
642				y += allocation.y;
643			}
644			if (x + requisition.width > Gdk.Screen.width ()) {
645				/* Adjust menu left */
646				int menu_width;
647				menu.get_preferred_width (out menu_width, null);
648				x = x - menu_width + allocation.y;
649			}
650		}
651
652		/**
653		 * build_menu:
654		 *
655		 * Build the window menu.
656		 */
657		private Gtk.Menu build_menu () {
658			var menu = new Gtk.Menu ();
659			menu.set_accel_group (this.ui.get_accel_group ());
660
661			var mi = new Gtk.MenuItem.with_mnemonic (_("_Groups"));
662			menu.append (mi);
663
664			/* Navigation */
665			var menu_go = new Gtk.Menu ();
666			menu_go.set_accel_group (this.ui.get_accel_group ());
667			menu_go.show.connect (update_menu_go);
668			mi.set_submenu (menu_go);
669
670			/* Note items */
671			mi = new Gtk.SeparatorMenuItem ();
672			menu.append (mi);
673
674			mi = new Gtk.MenuItem.with_mnemonic (_("_New"));
675			mi.set_accel_path (this.action_group.get_action ("new-note").get_accel_path ());
676			mi.activate.connect (action_new_note);
677			menu.append (mi);
678
679			mi = new Gtk.MenuItem.with_mnemonic (_("_Delete"));
680			mi.set_accel_path (this.action_group.get_action ("delete-note").get_accel_path ());
681			mi.activate.connect (action_delete_note);
682			menu.append (mi);
683
684			mi = new Gtk.MenuItem.with_mnemonic (_("_Rename"));
685			mi.set_accel_path (this.action_group.get_action ("rename-note").get_accel_path ());
686			mi.activate.connect (action_rename_note);
687			menu.append (mi);
688
689			mi = new Gtk.MenuItem.with_mnemonic (_("_Undo"));
690			mi.set_accel_path (this.action_group.get_action ("cancel").get_accel_path ());
691			mi.activate.connect (action_cancel);
692			menu.append (mi);
693
694			/* Window options */
695			mi = new Gtk.SeparatorMenuItem ();
696			menu.append (mi);
697
698			mi = this.mi_above = new Gtk.CheckMenuItem.with_label (_("Always on top"));
699			((Gtk.CheckMenuItem)mi).active = this.above;
700			((Gtk.CheckMenuItem)mi).toggled.connect ((o) => { above = o.active; });
701			menu.append (mi);
702
703			mi = this.mi_sticky = new Gtk.CheckMenuItem.with_label (_("Sticky window"));
704			((Gtk.CheckMenuItem)mi).active = this.sticky;
705			((Gtk.CheckMenuItem)mi).toggled.connect ((o) => { sticky = o.active; });
706			menu.append (mi);
707
708			/* Settings/About dialog */
709			mi = new Gtk.SeparatorMenuItem ();
710			menu.append (mi);
711
712			mi = new Gtk.MenuItem.with_mnemonic ("_Properties");
713			mi.activate.connect (() => { action ("properties"); });
714			menu.append (mi);
715
716			mi = new Gtk.MenuItem.with_mnemonic ("_About");
717			mi.activate.connect (() => { action ("about"); });
718			menu.append (mi);
719
720			return menu;
721		}
722
723		/**
724		 * update_menu_go:
725		 *
726		 * Update the menu Go when it is shown.
727		 */
728		private void update_menu_go (Gtk.Widget widget) {
729			Gtk.Menu menu = widget as Gtk.Menu;
730			Gtk.MenuItem mi;
731
732			menu.@foreach ((w) => {
733					w.destroy ();
734				});
735
736			foreach (var win in this.window_list) {
737				if (win == this) {
738					mi = new Gtk.MenuItem.with_label (win.name);
739					mi.sensitive = false;
740					menu.append (mi);
741
742					int current_page = this.notebook.get_current_page ();
743					var current_note = (Xnp.Note)(this.notebook.get_nth_page (current_page));
744					int n_pages = this.notebook.get_n_pages ();
745					for (int p = 0; p < n_pages; p++) {
746						var note = (Xnp.Note)(this.notebook.get_nth_page (p));
747						mi = new Gtk.MenuItem.with_label (note.name);
748						mi.set_data ("page", p.to_pointer ());
749						mi.activate.connect ((i) => {
750							int page = i.get_data<int> ("page");
751							notebook.set_current_page (page);
752						});
753						menu.append (mi);
754					}
755
756					mi = new Gtk.SeparatorMenuItem ();
757					menu.append (mi);
758				}
759				else {
760					mi = new Gtk.MenuItem.with_label (win.name);
761					mi.set_data ("window", (void*)win);
762					mi.activate.connect ((i) => {
763						var w = i.get_data<Xnp.Window> ("window");
764						w.present ();
765					});
766					menu.append (mi);
767
768					mi = new Gtk.SeparatorMenuItem ();
769					menu.append (mi);
770				}
771			}
772
773			mi = new Gtk.MenuItem.with_mnemonic (_("_Rename group"));
774			mi.set_accel_path (this.action_group.get_action ("rename-window").get_accel_path ());
775			mi.activate.connect (action_rename_window);
776			menu.append (mi);
777
778			mi = new Gtk.MenuItem.with_mnemonic (_("_Delete group"));
779			mi.set_accel_path (this.action_group.get_action ("delete-window").get_accel_path ());
780			mi.activate.connect (action_delete_window);
781			menu.append (mi);
782
783			mi = new Gtk.MenuItem.with_mnemonic (_("_Add a new group"));
784			mi.set_accel_path (this.action_group.get_action ("new-window").get_accel_path ());
785			mi.activate.connect (action_new_window);
786			menu.append (mi);
787
788			menu.show_all ();
789		}
790
791		/**
792		 * get_geometry:
793		 *
794		 * Returns the X,Y position and width/height.
795		 */
796		public void get_geometry (out int winx, out int winy, out int width, out int height) {
797			// Window is shaded
798			if (!this.content_box.get_visible ()) {
799				get_size (out this.width, null);
800			}
801			else {
802				get_size (out this.width, out this.height);
803			}
804			get_position (out winx, out winy);
805			width = this.width;
806			height = this.height;
807		}
808
809		/**
810		 * set_window_list:
811		 *
812		 * Saves a list of window inside window.window_list to be shown
813		 * within the window menu.
814		 */
815		public void set_window_list (SList <Xnp.Window> list) {
816			this.window_list = list;
817		}
818
819		/**
820		 * compare_func:
821		 *
822		 * Compare function for the window name to use with GLib.CompareFunc delegates.
823		 */
824		public int compare_func (Xnp.Window win2) {
825			return name.collate (win2.name);
826		}
827
828		/**
829		 * get_current_page:
830		 *
831		 * Get the current page in the notebook.
832		 */
833		public int get_current_page () {
834			return this.notebook.get_current_page ();
835		}
836
837		/**
838		 * set_current_page:
839		 *
840		 * Set the current page in the notebook.
841		 */
842		public void set_current_page (int page) {
843			this.notebook.set_current_page (page);
844		}
845
846		/*
847		 * Window management
848		 */
849
850		/**
851		 * shade:
852		 *
853		 * Shade the window (roll up) to show only the title bar.
854		 */
855		private void shade () {
856			if (this.content_box.get_visible ()) {
857				this.content_box.hide ();
858				get_size (out this.width, out this.height);
859				resize (this.width, 1);
860			}
861		}
862
863		/**
864		 * unshade:
865		 *
866		 * Unshade the window (roll down).
867		 */
868		private void unshade () {
869			if (!this.content_box.get_visible ()) {
870				this.content_box.show ();
871				get_size (out this.width, null);
872				resize (this.width, this.height);
873			}
874		}
875
876		/**
877		 * update_title:
878		 *
879		 * Updates the window title.
880		 */
881		private void update_title (string note_name) {
882			title = this.name + " - " + note_name;
883		}
884
885		/**
886		 * update_navigation_sensitivity:
887		 *
888		 * Update the goleft/right sensitivities.
889		 */
890		private void update_navigation_sensitivity (int page_num) {
891			int n_pages = notebook.get_n_pages ();
892			if (n_pages <= 1) {
893				this.left_arrow_button.sensitive = false;
894				this.right_arrow_button.sensitive = false;
895			}
896			else {
897				this.left_arrow_button.sensitive = page_num > 0 ? true : false;
898				this.right_arrow_button.sensitive = page_num + 1 < n_pages ? true : false;
899			}
900		}
901
902		/*
903		 * Note management
904		 */
905
906		/**
907		 * insert_note:
908		 *
909		 * Create a new note and insert it inside the notebook after
910		 * the current position.
911		 */
912		public Xnp.Note insert_note () {
913			int len = this.notebook.get_n_pages ();
914			string name = _("Notes");
915			for (int id = 1; id <= len + 1; id++) {
916				if (id > 1) {
917					name = _("Notes %d").printf (id);
918				}
919				if (!note_name_exists (name)) {
920					break;
921				}
922			}
923
924			int page = this.notebook.get_current_page () + 1;
925			var note = new Xnp.Note (name);
926
927			note.notify["name"].connect (note_notify_name_cb);
928			note.save_data.connect ((note) => { save_data (note); });
929
930			note.show ();
931			this.n_pages++;
932			this.notebook.insert_page (note, null, page);
933			this.notebook.set_tab_reorderable (note, true);
934			note.name = note.name; //note.notify ("name");
935			this.note_inserted (note);
936			_notebook_update_tabs_angle ();
937			return note;
938		}
939
940		/**
941		 * move_note:
942		 *
943		 * Moves the note named @note_name to position @page.
944		 */
945		public void move_note (string note_name, int page) {
946			int n_pages = this.notebook.get_n_pages ();
947			for (int p = 0; p < n_pages; p++) {
948				var note = (Xnp.Note)this.notebook.get_nth_page (p);
949				if (note.name == note_name) {
950					this.notebook.reorder_child (note, page);
951					update_navigation_sensitivity (page);
952					break;
953				}
954			}
955		}
956
957		/**
958		 * get_note_names:
959		 *
960		 * Returns a string list of the note names in the order they are currently displayed
961		 * in the notebook.
962		 */
963		public string[] get_note_names () {
964			string[] note_names = null;
965			int n_pages = this.notebook.get_n_pages ();
966			for (int p = 0; p < n_pages; p++) {
967				var note = (Xnp.Note)this.notebook.get_nth_page (p);
968				note_names += note.name;
969			}
970			return note_names;
971		}
972
973		/**
974		 * delete_current_note:
975		 *
976		 * Delete the current note.
977		 */
978		public void delete_current_note () {
979			this.delete_note (this.notebook.get_current_page ());
980		}
981
982		/**
983		 * delete_note:
984		 *
985		 * Delete note at page @page.
986		 */
987		public void delete_note (int page) {
988			var note = (Xnp.Note)this.notebook.get_nth_page (page);
989
990			if (note.text_view.buffer.get_char_count () > 0) {
991				var dialog = new Gtk.MessageDialog (this, Gtk.DialogFlags.DESTROY_WITH_PARENT,
992					Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("Are you sure you want to delete this note?"));
993				int res = dialog.run ();
994				dialog.destroy ();
995				if (res != Gtk.ResponseType.YES)
996					return;
997			}
998
999			this.n_pages--;
1000			this.notebook.remove_page (page);
1001			this.note_deleted (note);
1002			note.destroy ();
1003			if (this.notebook.get_n_pages () == 0)
1004				action ("delete");
1005		}
1006
1007		/**
1008		 * rename_current_note:
1009		 *
1010		 * Rename the current note.
1011		 */
1012		public void rename_current_note () {
1013			int page = this.notebook.get_current_page ();
1014			if (page == -1)
1015				return;
1016			var note = (Xnp.Note)(this.notebook.get_nth_page (page));
1017
1018			var dialog = new Gtk.Dialog.with_buttons (_("Rename note"), (Gtk.Window)get_toplevel (),
1019				Gtk.DialogFlags.MODAL|Gtk.DialogFlags.DESTROY_WITH_PARENT,
1020				"_Cancel", Gtk.ResponseType.CANCEL, "_OK", Gtk.ResponseType.OK);
1021			Gtk.Box content_area = (Gtk.Box)dialog.get_content_area ();
1022			dialog.set_default_response (Gtk.ResponseType.OK);
1023			dialog.resizable = false;
1024			dialog.icon_name = "gtk-edit";
1025			dialog.border_width = 4;
1026			content_area.border_width = 6;
1027
1028			var entry = new Gtk.Entry ();
1029			entry.text = note.name;
1030			entry.activates_default = true;
1031			content_area.add (entry);
1032			content_area.show_all ();
1033
1034			int res = dialog.run ();
1035			dialog.hide ();
1036			if (res == Gtk.ResponseType.OK) {
1037				string name = entry.text;
1038				if (note_name_exists (name)) {
1039					var error_dialog = new Gtk.MessageDialog (this, Gtk.DialogFlags.DESTROY_WITH_PARENT,
1040						Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, _("The name %s is already in use"), name);
1041					error_dialog.run ();
1042					error_dialog.destroy ();
1043				}
1044				else {
1045					string old_name = note.name;
1046					note.name = name;
1047					this.note_renamed (note, old_name);
1048				}
1049			}
1050			dialog.destroy ();
1051		}
1052
1053		/**
1054		 * set_font:
1055		 *
1056		 * Set the font for the window.
1057		 */
1058		public void set_font () {
1059			int page = this.notebook.get_current_page ();
1060			if (page == -1)
1061				return;
1062			var note = (Xnp.Note)(this.notebook.get_nth_page (page));
1063
1064			var dialog = new Gtk.FontChooserDialog ("Choose current note font", this);
1065			dialog.set_font (note.text_view.font);
1066			int res = dialog.run ();
1067			dialog.hide ();
1068			if (res == Gtk.ResponseType.OK) {
1069				note.text_view.font = dialog.get_font ();
1070			}
1071			dialog.destroy ();
1072		}
1073
1074		/**
1075		 * note_name_exists:
1076		 *
1077		 * Verify if the given name already exists in the notebook.
1078		 */
1079		private bool note_name_exists (string name) {
1080			int n_pages = this.notebook.get_n_pages ();
1081			for (int p = 0; p < n_pages; p++) {
1082				var note = (Xnp.Note)this.notebook.get_nth_page (p);
1083				if (note.name == name) {
1084					return true;
1085				}
1086			}
1087			return false;
1088		}
1089
1090		/**
1091		 * save_notes:
1092		 *
1093		 * Send the save-data signal on every dirty note.
1094		 */
1095		public void save_notes () {
1096			int n_pages = this.notebook.get_n_pages ();
1097			for (int p = 0; p < n_pages; p++) {
1098				var note = (Xnp.Note)this.notebook.get_nth_page (p);
1099				if (note.dirty) {
1100					note.dirty = false;
1101					save_data (note);
1102				}
1103			}
1104		}
1105
1106		/**
1107		 * _notebook_update_tabs_angle:
1108		 *
1109		 * Set the angle of each label in the tab.
1110		 */
1111		private void _notebook_update_tabs_angle () {
1112			int angle = 0;
1113			if (_tabs_position == 2)
1114				angle = 270;
1115			else if (_tabs_position == 4)
1116				angle = 90;
1117
1118			int pages = this.notebook.get_n_pages ();
1119			for (int i = 0; i < pages; i++) {
1120				var widget = this.notebook.get_nth_page (i);
1121				var label = this.notebook.get_tab_label (widget) as Gtk.Label;
1122				if (label is Gtk.Label) {
1123					label.angle = angle;
1124				}
1125			}
1126		}
1127
1128/* valac -X '-I..' -X '-DGETTEXT_PACKAGE="xfce4-notes-plugin"' -X '-DPKGDATADIR="../data"' -D DEBUG_XNP_WINDOW --pkg=gtk+-3.0 --pkg=libxfce4util-1.0 --pkg=libxfconf-0 --pkg=color --pkg=config --vapidir=.. --vapidir=. window.vala note.vala hypertextview.vala icon-button.vala */
1129#if DEBUG_XNP_WINDOW
1130		static int main (string[] args) {
1131			Gtk.init (ref args);
1132			var sample = new Xnp.Window ();
1133			sample.show ();
1134			Gtk.main ();
1135			return 0;
1136		}
1137#endif
1138	}
1139
1140}
1141