Lomiri
IndicatorMenuItemFactory.qml
1 /*
2  * Copyright 2013-2016 Canonical Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.12
18 import QtQuick.Window 2.2
19 import Lomiri.Settings.Menus 0.1 as Menus
20 import Lomiri.Settings.Components 0.1
21 import QMenuModel 1.0
22 import Utils 0.1 as Utils
23 import Lomiri.Components.ListItems 1.3 as ListItems
24 import Lomiri.Components 1.3
25 import Lomiri.Session 0.1
26 import Lomiri.Platform 1.0
27 
28 Item {
29  id: menuFactory
30 
31  property string indicator
32  property var rootModel: null
33  property var menuModel: null
34 
35  property var _userMap: null
36  readonly property var _typeToComponent: {
37  "default": {
38  "lomiri.widgets.systemsettings.tablet.volumecontrol" : sliderMenu,
39  "lomiri.widgets.systemsettings.tablet.switch" : switchMenu,
40 
41  "com.canonical.indicator.button" : buttonMenu,
42  "com.canonical.indicator.div" : separatorMenu,
43  "com.canonical.indicator.section" : sectionMenu,
44  "com.canonical.indicator.progress" : progressMenu,
45  "com.canonical.indicator.slider" : sliderMenu,
46  "com.canonical.indicator.switch" : switchMenu,
47  "com.canonical.indicator.alarm" : alarmMenu,
48  "com.canonical.indicator.appointment" : appointmentMenu,
49  "com.canonical.indicator.transfer" : transferMenu,
50  "com.canonical.indicator.button-section" : buttonSectionMenu,
51  "com.canonical.indicator.link" : linkMenu,
52 
53  "com.canonical.indicator.messages.messageitem" : messageItem,
54  "com.canonical.indicator.messages.sourceitem" : groupedMessage,
55 
56  "com.canonical.lomiri.slider" : sliderMenu,
57  "com.canonical.lomiri.switch" : switchMenu,
58 
59  "com.canonical.lomiri.media-player" : mediaPayerMenu,
60  "com.canonical.lomiri.playback-item" : playbackItemMenu,
61 
62  "lomiri.widgets.systemsettings.tablet.wifisection" : wifiSection,
63  "lomiri.widgets.systemsettings.tablet.accesspoint" : accessPoint,
64  "com.lomiri.indicator.network.modeminfoitem" : modeminfoitem,
65 
66  "com.canonical.indicator.calendar": calendarMenu,
67  "com.canonical.indicator.location": timezoneMenu,
68 
69  "com.lomiri.indicator.transfer" : transferMenu,
70 
71  "org.ayatana.indicator.button" : buttonMenu,
72  "org.ayatana.indicator.div" : separatorMenu,
73  "org.ayatana.indicator.section" : sectionMenu,
74  "org.ayatana.indicator.progress" : progressMenu,
75  "org.ayatana.indicator.slider" : sliderMenu,
76  "org.ayatana.indicator.switch" : switchMenu,
77  "org.ayatana.indicator.alarm" : alarmMenu,
78  "org.ayatana.indicator.appointment" : appointmentMenu,
79  "org.ayatana.indicator.transfer" : transferMenu,
80  "org.ayatana.indicator.button-section" : buttonSectionMenu,
81  "org.ayatana.indicator.link" : linkMenu,
82 
83  "org.ayatana.indicator.messages.messageitem" : messageItem,
84  "org.ayatana.indicator.messages.sourceitem" : groupedMessage,
85 
86  "org.ayatana.indicator.slider" : sliderMenu,
87  "org.ayatana.indicator.switch" : switchMenu,
88 
89  "org.ayatana.indicator.media-player" : mediaPayerMenu,
90  "org.ayatana.indicator.playback-item" : playbackItemMenu,
91 
92  "org.ayatana.indicator.network.modeminfoitem" : modeminfoitem,
93 
94  "org.ayatana.indicator.calendar": calendarMenu,
95  "org.ayatana.indicator.location": timezoneMenu,
96  "org.ayatana.indicator.level" : levelMenu,
97  },
98  "indicator-session": {
99  "indicator.user-menu-item": Platform.isPC ? userMenuItem : null,
100  "indicator.guest-menu-item": Platform.isPC ? userMenuItem : null,
101  "com.canonical.indicator.switch": Math.min(Screen.width, Screen.height) > units.gu(60) ? switchMenu : null // Desktop mode switch
102  },
103  "indicator-messages": {
104  "com.canonical.indicator.button": messagesButtonMenu
105  },
106  "ayatana-indicator-session": {
107  "org.ayatana.indicator.user-menu-item": Platform.isPC ? userMenuItem : null,
108  "org.ayatana.indicator.guest-menu-item": Platform.isPC ? userMenuItem : null,
109  "org.ayatana.indicator.switch": Math.min(Screen.width, Screen.height) > units.gu(60) ? switchMenu : null // Desktop mode switch
110  },
111  "ayatana-indicator-messages": {
112  "org.ayatana.indicator.button": messagesButtonMenu
113  }
114  }
115 
116  readonly property var _action_filter_map: {
117  "indicator-session": {
118  "indicator.logout": Platform.isPC ? undefined : null,
119  "indicator.suspend": Platform.isPC ? undefined : null,
120  "indicator.hibernate": Platform.isPC ? undefined : null,
121  "indicator.reboot": Platform.isPC ? undefined : null
122  },
123  "indicator-keyboard": {
124  "indicator.map": null,
125  "indicator.chart": null
126  },
127  "ayatana-indicator-session": {
128  "indicator.logout": Platform.isPC ? undefined : null,
129  "indicator.suspend": Platform.isPC ? undefined : null,
130  "indicator.hibernate": Platform.isPC ? undefined : null,
131  "indicator.reboot": Platform.isPC ? undefined : null
132  },
133  "ayatana-indicator-keyboard": {
134  "indicator.map": null,
135  "indicator.chart": null
136  }
137  }
138 
139  function getComponentForIndicatorEntryType(type) {
140  var component = undefined;
141  var map = _userMap || _typeToComponent
142  var indicatorComponents = map[indicator];
143 
144  if (type === undefined || type === "") {
145  return component
146  }
147 
148  if (indicatorComponents !== undefined) {
149  component = indicatorComponents[type];
150  }
151 
152  if (component === undefined) {
153  component = map["default"][type];
154  }
155 
156  if (component === undefined) {
157  console.debug("Don't know how to make " + type + " for " + indicator);
158  }
159 
160  return component
161  }
162 
163  function getComponentForIndicatorEntryAction(action) {
164  var component = undefined;
165  var indicatorFilter = _action_filter_map[indicator]
166 
167  if (action === undefined || action === "") {
168  return component
169  }
170 
171  if (indicatorFilter !== undefined) {
172  component = indicatorFilter[action];
173  }
174  return component
175  }
176 
177  function getExtendedProperty(object, propertyName, defaultValue) {
178  if (object && object.hasOwnProperty(propertyName)) {
179  return object[propertyName];
180  }
181  return defaultValue;
182  }
183 
184  Component {
185  id: separatorMenu;
186 
187  Menus.SeparatorMenu {
188  objectName: "separatorMenu"
189  }
190  }
191 
192  Component {
193  id: sliderMenu;
194 
195  Menus.SliderMenu {
196  id: sliderItem
197  objectName: "sliderMenu"
198  property QtObject menuData: null
199  property var menuModel: menuFactory.menuModel
200  property int menuIndex: -1
201  property var extendedData: menuData && menuData.ext || undefined
202  property var serverValue: getExtendedProperty(menuData, "actionState", undefined)
203 
204  text: menuData && menuData.label || ""
205  minIcon: getExtendedProperty(extendedData, "minIcon", "")
206  maxIcon: getExtendedProperty(extendedData, "maxIcon", "")
207 
208  minimumValue: getExtendedProperty(extendedData, "minValue", 0.0)
209  maximumValue: {
210  var maximum = getExtendedProperty(extendedData, "maxValue", 1.0);
211  if (maximum <= minimumValue) {
212  return minimumValue + 1;
213  }
214  return maximum;
215  }
216  enabled: menuData && menuData.sensitive || false
217  highlightWhenPressed: false
218 
219  onMenuModelChanged: {
220  loadAttributes();
221  }
222  onMenuIndexChanged: {
223  loadAttributes();
224  }
225 
226  function loadAttributes() {
227  if (!menuModel || menuIndex == -1) return;
228  menuModel.loadExtendedAttributes(menuIndex, {'min-value': 'double',
229  'max-value': 'double',
230  'min-icon': 'icon',
231  'max-icon': 'icon',
232  'x-ayatana-sync-action': 'string'});
233  }
234 
235  ServerPropertySynchroniser {
236  id: sliderPropertySync
237  objectName: "sync"
238  syncTimeout: Utils.Constants.indicatorValueTimeout
239  bufferedSyncTimeout: true
240  maximumWaitBufferInterval: 16
241 
242  serverTarget: sliderItem
243  serverProperty: "serverValue"
244  userTarget: sliderItem
245  userProperty: "value"
246 
247  onSyncTriggered: menuModel.changeState(menuIndex, value)
248  }
249 
250  AyatanaMenuAction {
251  model: menuModel
252  index: menuIndex
253  name: getExtendedProperty(extendedData, "xAyatanaSyncAction", "")
254  onStateChanged: {
255  sliderPropertySync.reset();
256  sliderPropertySync.updateUserValue();
257  }
258  }
259  }
260  }
261 
262  Component {
263  id: buttonMenu;
264 
265  Menus.ButtonMenu {
266  objectName: "buttonMenu"
267  property QtObject menuData: null
268  property var menuModel: menuFactory.menuModel
269  property int menuIndex: -1
270 
271  buttonText: menuData && menuData.label || ""
272  enabled: menuData && menuData.sensitive || false
273  highlightWhenPressed: false
274 
275  onTriggered: {
276  menuModel.activate(menuIndex);
277  }
278  }
279  }
280 
281  Component {
282  id: messagesButtonMenu;
283 
284  Menus.BaseLayoutMenu {
285  objectName: "messagesButtonMenu"
286  property QtObject menuData: null
287  property var menuModel: menuFactory.menuModel
288  property int menuIndex: -1
289 
290  highlightWhenPressed: false
291  enabled: menuData && menuData.sensitive || false
292  text: menuData && menuData.label || ""
293  title.color: theme.palette.selected.backgroundText
294  title.horizontalAlignment: Text.AlignHCenter
295  title.font.bold: true
296 
297  onClicked: menuModel.activate(menuIndex);
298  }
299  }
300 
301  Component {
302  id: sectionMenu;
303 
304  Menus.SectionMenu {
305  objectName: "sectionMenu"
306  property QtObject menuData: null
307  property var menuIndex: undefined
308 
309  text: menuData && menuData.label || ""
310  busy: false
311  }
312  }
313 
314  Component {
315  id: progressMenu;
316 
317  Menus.ProgressValueMenu {
318  objectName: "progressMenu"
319  property QtObject menuData: null
320  property int menuIndex: -1
321 
322  text: menuData && menuData.label || ""
323  iconSource: menuData && menuData.icon || ""
324  value : menuData && menuData.actionState || 0.0
325  enabled: menuData && menuData.sensitive || false
326  }
327  }
328 
329  Component {
330  id: levelMenu;
331 
332  /* Use the same UI as progressMenu for now. */
333  Menus.ProgressValueMenu {
334  objectName: "levelMenu"
335  property QtObject menuData: null
336  property var menuModel: menuFactory.menuModel
337  property int menuIndex: -1
338  property var extendedData: menuData && menuData.ext || undefined
339 
340  text: menuData && menuData.label || ""
341  iconSource: menuData && menuData.icon || ""
342  value : extendedData && extendedData.xAyatanaLevel || 0.0
343  enabled: menuData && menuData.sensitive || false
344 
345  onMenuModelChanged: {
346  loadAttributes();
347  }
348  onMenuIndexChanged: {
349  loadAttributes();
350  }
351 
352  function loadAttributes() {
353  if (!menuModel || menuIndex == -1) return;
354  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-level': 'uint16'});
355  }
356  }
357  }
358 
359  Component {
360  id: standardMenu;
361 
362  Menus.StandardMenu {
363  objectName: "standardMenu"
364  property QtObject menuData: null
365  property int menuIndex: -1
366 
367  text: menuData && menuData.label || ""
368  iconSource: menuData && menuData.icon || ""
369  enabled: menuData && menuData.sensitive || false
370  highlightWhenPressed: false
371 
372  onTriggered: {
373  menuModel.activate(menuIndex);
374  }
375  }
376  }
377 
378  Component {
379  id: linkMenu;
380 
381  Menus.BaseLayoutMenu {
382  objectName: "linkMenu"
383  property QtObject menuData: null
384  property int menuIndex: -1
385 
386  text: menuData && menuData.label || ""
387  enabled: menuData && menuData.sensitive || false
388  backColor: Qt.rgba(1,1,1,0.07)
389  highlightWhenPressed: false
390 
391  onTriggered: {
392  menuModel.activate(menuIndex);
393  }
394 
395  slots: Icon {
396  source: {
397  if (menuData) {
398  if (menuData.icon && menuData.icon != "") {
399  return menuData.icon
400  } else if (menuData.action.indexOf("settings") > -1) {
401  return "image://theme/settings"
402  }
403  }
404  return ""
405  }
406  height: units.gu(3)
407  width: height
408  color: theme.palette.normal.backgroundText
409  SlotsLayout.position: SlotsLayout.Trailing
410  }
411  }
412  }
413 
414  Component {
415  id: checkableMenu;
416 
417  Menus.CheckableMenu {
418  id: checkItem
419  objectName: "checkableMenu"
420  property QtObject menuData: null
421  property int menuIndex: -1
422  property bool serverChecked: menuData && menuData.isToggled || false
423 
424  text: menuData && menuData.label || ""
425  enabled: menuData && menuData.sensitive || false
426  checked: serverChecked
427  highlightWhenPressed: false
428 
429  ServerPropertySynchroniser {
430  objectName: "sync"
431  syncTimeout: Utils.Constants.indicatorValueTimeout
432 
433  serverTarget: checkItem
434  serverProperty: "serverChecked"
435  userTarget: checkItem
436  userProperty: "checked"
437 
438  onSyncTriggered: menuModel.activate(checkItem.menuIndex)
439  }
440  }
441  }
442 
443  Component {
444  id: radioMenu;
445 
446  Menus.RadioMenu {
447  id: radioItem
448  objectName: "radioMenu"
449  property QtObject menuData: null
450  property int menuIndex: -1
451  property bool serverChecked: menuData && menuData.isToggled || false
452 
453  text: menuData && menuData.label || ""
454  enabled: menuData && menuData.sensitive || false
455  checked: serverChecked
456  highlightWhenPressed: false
457 
458  ServerPropertySynchroniser {
459  objectName: "sync"
460  syncTimeout: Utils.Constants.indicatorValueTimeout
461 
462  serverTarget: radioItem
463  serverProperty: "serverChecked"
464  userTarget: radioItem
465  userProperty: "checked"
466 
467  onSyncTriggered: menuModel.activate(radioItem.menuIndex)
468  }
469  }
470  }
471 
472  Component {
473  id: switchMenu;
474 
475  Menus.SwitchMenu {
476  id: switchItem
477  objectName: "switchMenu"
478  property QtObject menuData: null
479  property var menuModel: menuFactory.menuModel
480  property int menuIndex: -1
481  property var extendedData: menuData && menuData.ext || undefined
482  property bool serverChecked: menuData && menuData.isToggled || false
483 
484  text: menuData && menuData.label || ""
485  iconSource: menuData && menuData.icon || ""
486  enabled: menuData && menuData.sensitive || false
487  checked: serverChecked
488  highlightWhenPressed: false
489 
490  property var subtitleAction: AyatanaMenuAction {
491  model: menuModel
492  index: menuIndex
493  name: getExtendedProperty(extendedData, "xAyatanaSubtitleAction", "")
494  }
495  subtitle.text: subtitleAction.valid ? subtitleAction.state : ""
496 
497  onMenuModelChanged: {
498  loadAttributes();
499  }
500  onMenuIndexChanged: {
501  loadAttributes();
502  }
503 
504  function loadAttributes() {
505  if (!menuModel || menuIndex == -1) return;
506  menuModel.loadExtendedAttributes(menuIndex, {
507  'x-ayatana-subtitle-action': 'string',
508  'target': 'bool',
509  });
510  }
511 
512  ServerPropertySynchroniser {
513  objectName: "sync"
514  syncTimeout: Utils.Constants.indicatorValueTimeout
515 
516  serverTarget: switchItem
517  serverProperty: "serverChecked"
518  userTarget: switchItem
519  userProperty: "checked"
520 
521  onSyncTriggered: {
522  /* Figures out if the action's activate() requires a
523  * parameter or not. Works with:
524  * - com.canonical.indicator.switch
525  * - org.ayatana.indicator.switch (with fix)
526  * - com.canonical.indicator.switch mis-labled as
527  * org.ayatana.indicator.switch
528  * https://gitlab.com/ubports/development/core/lomiri-indicator-network/-/issues/87#note_1206883970
529  *
530  * If activate() requires a parameter but menu doesn't
531  * specify a target, the menu will be broken in a different
532  * way anyway.
533  */
534  if (extendedData.hasOwnProperty('target')) {
535  menuModel.activate(switchItem.menuIndex, switchItem.checked);
536  } else {
537  menuModel.activate(switchItem.menuIndex);
538  }
539  }
540  }
541  }
542  }
543 
544  Component {
545  id: alarmMenu;
546 
547  Menus.EventMenu {
548  id: alarmItem
549  objectName: "alarmMenu"
550  property QtObject menuData: null
551  property var menuModel: menuFactory.menuModel
552  property int menuIndex: -1
553  property var extendedData: menuData && menuData.ext || undefined
554 
555  readonly property date serverTime: new Date(getExtendedProperty(extendedData, "xAyatanaTime", 0) * 1000)
556  LiveTimer {
557  frequency: LiveTimer.Relative
558  relativeTime: alarmItem.serverTime
559  onTrigger: alarmItem.time = i18n.relativeDateTime(alarmItem.serverTime)
560  }
561 
562  text: menuData && menuData.label || ""
563  iconSource: menuData && menuData.icon || "image://theme/alarm-clock"
564  time: i18n.relativeDateTime(serverTime)
565  enabled: menuData && menuData.sensitive || false
566  highlightWhenPressed: false
567 
568  onMenuModelChanged: {
569  loadAttributes();
570  }
571  onMenuIndexChanged: {
572  loadAttributes();
573  }
574  onTriggered: {
575  menuModel.activate(menuIndex);
576  }
577 
578  function loadAttributes() {
579  if (!menuModel || menuIndex == -1) return;
580  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-time': 'int64'});
581  }
582  }
583  }
584 
585  Component {
586  id: appointmentMenu;
587 
588  Menus.EventMenu {
589  id: appointmentItem
590  objectName: "appointmentMenu"
591  property QtObject menuData: null
592  property var menuModel: menuFactory.menuModel
593  property int menuIndex: -1
594  property var extendedData: menuData && menuData.ext || undefined
595 
596  readonly property date serverTime: new Date(getExtendedProperty(extendedData, "xAyatanaTime", 0) * 1000)
597 
598  LiveTimer {
599  frequency: LiveTimer.Relative
600  relativeTime: appointmentItem.serverTime
601  onTrigger: appointmentItem.time = i18n.relativeDateTime(appointmentItem.serverTime)
602  }
603 
604  text: menuData && menuData.label || ""
605  iconSource: menuData && menuData.icon || "image://theme/calendar"
606  time: i18n.relativeDateTime(serverTime)
607  eventColor: getExtendedProperty(extendedData, "xAyatanaColor", Qt.rgba(0.0, 0.0, 0.0, 0.0))
608  enabled: menuData && menuData.sensitive || false
609  highlightWhenPressed: false
610 
611  onMenuModelChanged: {
612  loadAttributes();
613  }
614  onMenuIndexChanged: {
615  loadAttributes();
616  }
617  onTriggered: {
618  menuModel.activate(menuIndex);
619  }
620 
621  function loadAttributes() {
622  if (!menuModel || menuIndex == -1) return;
623  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-color': 'string',
624  'x-ayatana-time': 'int64'});
625  }
626  }
627  }
628 
629  Component {
630  id: userMenuItem
631 
632  Menus.UserSessionMenu {
633  objectName: "userSessionMenu"
634  highlightWhenPressed: false
635 
636  property QtObject menuData: null
637  property var menuModel: menuFactory.menuModel
638  property int menuIndex: -1
639 
640  name: menuData && menuData.label || "" // label is the user's real name
641  iconSource: menuData && menuData.icon || ""
642 
643  // would be better to compare with the logname but sadly the indicator doesn't expose that
644  active: DBusLomiriSessionService.RealName() !== "" ? DBusLomiriSessionService.RealName() == name
645  : DBusLomiriSessionService.UserName() == name
646 
647  onTriggered: {
648  menuModel.activate(menuIndex);
649  }
650  }
651  }
652 
653  Component {
654  id: calendarMenu
655 
656  Menus.CalendarMenu {
657  id: calendarItem
658  objectName: "calendarMenu"
659  focus: true
660 
661  property QtObject menuData: null
662  property var menuModel: menuFactory.menuModel
663  property var actionState: menuData && menuData.actionState || null
664  property real calendarDay: getExtendedProperty(actionState, "calendar-day", 0)
665  property int menuIndex: -1
666 
667  showWeekNumbers: getExtendedProperty(actionState, "show-week-numbers", false)
668  eventDays: getExtendedProperty(actionState, "appointment-days", [])
669 
670  onCalendarDayChanged: {
671  if (calendarDay > 0) {
672  // This would trigger a selectionDateChanged signal, thus
673  // we've to avoid that the subsequent model activation
674  // would cause an infinite loop
675  modelUpdateConnections.enabled = false
676  currentDate = new Date(calendarDay * 1000)
677  modelUpdateConnections.enabled = true
678  }
679  }
680 
681  Connections {
682  id: modelUpdateConnections
683  property bool enabled: true
684  target: (enabled && calendarItem.visible) ? calendarItem : null
685 
686  onSelectedDateChanged: {
687  menuModel.activate(menuIndex, selectedDate.getTime() / 1000 | 0)
688  }
689  }
690  }
691  }
692 
693  Component {
694  id: timezoneMenu
695 
696  Menus.TimeZoneMenu {
697  id: tzMenuItem
698  objectName: "timezoneMenu"
699 
700  property QtObject menuData: null
701  property var menuModel: menuFactory.menuModel
702  property int menuIndex: -1
703  property var extendedData: menuData && menuData.ext || undefined
704  readonly property string tz: getExtendedProperty(extendedData, "xAyatanaTimezone", "UTC")
705  property var updateTimer: Timer {
706  repeat: true
707  running: tzMenuItem.visible // only run when we're open
708  onTriggered: tzMenuItem.time = Utils.TimezoneFormatter.currentTimeInTimezone(tzMenuItem.tz)
709  }
710 
711  city: menuData && menuData.label || ""
712  time: Utils.TimezoneFormatter.currentTimeInTimezone(tz)
713  enabled: menuData && menuData.sensitive || false
714 
715  onMenuModelChanged: {
716  loadAttributes();
717  }
718  onMenuIndexChanged: {
719  loadAttributes();
720  }
721  onTriggered: {
722  tzActionGroup.setLocation.activate(tz);
723  }
724 
725  QDBusActionGroup {
726  id: tzActionGroup
727  busType: DBus.SessionBus
728  busName: "org.ayatana.indicator.datetime"
729  objectPath: "/org/ayatana/indicator/datetime"
730 
731  property variant setLocation: action("set-location")
732 
733  Component.onCompleted: tzActionGroup.start()
734  }
735 
736  function loadAttributes() {
737  if (!menuModel || menuIndex == -1) return;
738  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-timezone': 'string'});
739  }
740  }
741  }
742 
743  Component {
744  id: wifiSection;
745 
746  Menus.SectionMenu {
747  objectName: "wifiSection"
748  property QtObject menuData: null
749  property var menuModel: menuFactory.menuModel
750  property int menuIndex: -1
751  property var extendedData: menuData && menuData.ext || undefined
752 
753  text: menuData && menuData.label || ""
754  busy: getExtendedProperty(extendedData, "xCanonicalBusyAction", false)
755 
756  onMenuModelChanged: {
757  loadAttributes();
758  }
759  onMenuIndexChanged: {
760  loadAttributes();
761  }
762 
763  function loadAttributes() {
764  if (!menuModel || menuIndex == -1) return;
765  menuModel.loadExtendedAttributes(menuIndex, {'x-canonical-busy-action': 'bool'})
766  }
767  }
768  }
769 
770  Component {
771  id: accessPoint;
772 
773  Menus.AccessPointMenu {
774  id: apItem
775  objectName: "accessPoint"
776  property QtObject menuData: null
777  property var menuModel: menuFactory.menuModel
778  property int menuIndex: -1
779  property var extendedData: menuData && menuData.ext || undefined
780  property bool serverChecked: menuData && menuData.isToggled || false
781 
782  property var strengthAction: AyatanaMenuAction {
783  model: menuModel
784  index: menuIndex
785  name: getExtendedProperty(extendedData, "xAyatanaWifiApStrengthAction", "")
786  }
787 
788  text: menuData && menuData.label || ""
789  enabled: menuData && menuData.sensitive || false
790  active: serverChecked
791  secure: getExtendedProperty(extendedData, "xAyatanaWifiApIsSecure", false)
792  adHoc: getExtendedProperty(extendedData, "xAyatanaWifiApIsAdhoc", false)
793  signalStrength: {
794  if (strengthAction.valid) {
795  var state = strengthAction.state; // handle both int and uchar
796  // FIXME remove the special casing when we switch to indicator-network completely
797  if (typeof state == "string") {
798  return state.charCodeAt();
799  }
800  return state;
801  }
802  return 0;
803  }
804  highlightWhenPressed: false
805 
806  onMenuModelChanged: {
807  loadAttributes();
808  }
809  onMenuIndexChanged: {
810  loadAttributes();
811  }
812 
813  function loadAttributes() {
814  if (!menuModel || menuIndex == -1) return;
815  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-wifi-ap-is-adhoc': 'bool',
816  'x-ayatana-wifi-ap-is-secure': 'bool',
817  'x-ayatana-wifi-ap-strength-action': 'string'});
818  }
819 
820  ServerPropertySynchroniser {
821  objectName: "sync"
822  syncTimeout: Utils.Constants.indicatorValueTimeout
823 
824  serverTarget: apItem
825  serverProperty: "serverChecked"
826  userTarget: apItem
827  userProperty: "active"
828  userTrigger: "onTriggered"
829 
830  onSyncTriggered: menuModel.activate(apItem.menuIndex)
831  }
832  }
833  }
834 
835  Component {
836  id: modeminfoitem;
837  Menus.ModemInfoItem {
838  objectName: "modemInfoItem"
839  property QtObject menuData: null
840  property var menuModel: menuFactory.menuModel
841  property int menuIndex: -1
842  property var extendedData: menuData && menuData.ext || undefined
843  highlightWhenPressed: false
844 
845  property var statusLabelAction: AyatanaMenuAction {
846  model: menuModel
847  index: menuIndex
848  name: getExtendedProperty(extendedData, "xLomiriModemStatusLabelAction", "")
849  }
850  statusText: statusLabelAction.valid ? statusLabelAction.state : ""
851 
852  property var statusIconAction: AyatanaMenuAction {
853  model: menuModel
854  index: menuIndex
855  name: getExtendedProperty(extendedData, "xLomiriModemStatusIconAction", "")
856  }
857  statusIcon: statusIconAction.valid ? statusIconAction.state : ""
858 
859  property var connectivityIconAction: AyatanaMenuAction {
860  model: menuModel
861  index: menuIndex
862  name: getExtendedProperty(extendedData, "xLomiriModemConnectivityIconAction", "")
863  }
864  connectivityIcon: connectivityIconAction.valid ? connectivityIconAction.state : ""
865 
866  property var simIdentifierLabelAction: AyatanaMenuAction {
867  model: menuModel
868  index: menuIndex
869  name: getExtendedProperty(extendedData, "xLomiriModemSimIdentifierLabelAction", "")
870  }
871  simIdentifierText: simIdentifierLabelAction.valid ? simIdentifierLabelAction.state : ""
872 
873  property var roamingAction: AyatanaMenuAction {
874  model: menuModel
875  index: menuIndex
876  name: getExtendedProperty(extendedData, "xLomiriModemRoamingAction", "")
877  }
878  roaming: roamingAction.valid ? roamingAction.state : false
879 
880  property var unlockAction: AyatanaMenuAction {
881  model: menuModel
882  index: menuIndex
883  name: getExtendedProperty(extendedData, "xLomiriModemLockedAction", "")
884  }
885  onUnlock: {
886  unlockAction.activate();
887  }
888  locked: unlockAction.valid ? unlockAction.state : false
889 
890  onMenuModelChanged: {
891  loadAttributes();
892  }
893  onMenuIndexChanged: {
894  loadAttributes();
895  }
896 
897  function loadAttributes() {
898  if (!menuModel || menuIndex == -1) return;
899  menuModel.loadExtendedAttributes(menuIndex, {'x-lomiri-modem-status-label-action': 'string',
900  'x-lomiri-modem-status-icon-action': 'string',
901  'x-lomiri-modem-connectivity-icon-action': 'string',
902  'x-lomiri-modem-sim-identifier-label-action': 'string',
903  'x-lomiri-modem-roaming-action': 'string',
904  'x-lomiri-modem-locked-action': 'string'});
905  }
906  }
907  }
908 
909  Component {
910  id: messageItem
911 
912  MessageMenuItemFactory {
913  objectName: "messageItem"
914  menuModel: menuFactory.menuModel
915  }
916  }
917 
918  Component {
919  id: groupedMessage
920 
921  Menus.GroupedMessageMenu {
922  objectName: "groupedMessage"
923  property QtObject menuData: null
924  property var menuModel: menuFactory.menuModel
925  property int menuIndex: -1
926  property var extendedData: menuData && menuData.ext || undefined
927 
928  text: menuData && menuData.label || ""
929  iconSource: getExtendedProperty(extendedData, "icon", "image://theme/message")
930  count: menuData && menuData.actionState.length > 0 ? menuData.actionState[0] : "0"
931  enabled: menuData && menuData.sensitive || false
932  highlightWhenPressed: false
933  removable: true
934 
935  onMenuModelChanged: {
936  loadAttributes();
937  }
938  onMenuIndexChanged: {
939  loadAttributes();
940  }
941  onClicked: {
942  menuModel.activate(menuIndex, true);
943  }
944  onDismissed: {
945  menuModel.activate(menuIndex, false);
946  }
947 
948  function loadAttributes() {
949  if (!menuModel || menuIndex == -1) return;
950  menuModel.loadExtendedAttributes(modelIndex, {'icon': 'icon'});
951  }
952  }
953  }
954 
955  Component {
956  id: mediaPayerMenu;
957 
958  Menus.MediaPlayerMenu {
959  objectName: "mediaPayerMenu"
960  property QtObject menuData: null
961  property var menuModel: menuFactory.menuModel
962  property int menuIndex: -1
963  property var actionState: menuData && menuData.actionState || undefined
964  property bool running: getExtendedProperty(actionState, "running", false)
965 
966  playerIcon: menuData && menuData.icon || "image://theme/stock_music"
967  playerName: menuData && menuData.label || i18n.tr("Nothing is playing")
968 
969  albumArt: getExtendedProperty(actionState, "art-url", "image://theme/stock_music")
970  song: getExtendedProperty(actionState, "title", "")
971  artist: getExtendedProperty(actionState, "artist", "")
972  album: getExtendedProperty(actionState, "album", "")
973  showTrack: running && (state == "Playing" || state == "Paused")
974  state: getExtendedProperty(actionState, "state", "")
975  enabled: menuData && menuData.sensitive || false
976  highlightWhenPressed: false
977 
978  onTriggered: {
979  model.activate(modelIndex);
980  }
981  }
982  }
983 
984  Component {
985  id: playbackItemMenu;
986 
987  Menus.PlaybackItemMenu {
988  objectName: "playbackItemMenu"
989  property QtObject menuData: null
990  property var menuModel: menuFactory.menuModel
991  property int menuIndex: -1
992  property var extendedData: menuData && menuData.ext || undefined
993 
994  property var playAction: AyatanaMenuAction {
995  model: menuModel
996  index: menuIndex
997  name: getExtendedProperty(extendedData, "xAyatanaPlayAction", "")
998  }
999  property var nextAction: AyatanaMenuAction {
1000  model: menuModel
1001  index: menuIndex
1002  name: getExtendedProperty(extendedData, "xAyatanaNextAction", "")
1003  }
1004  property var previousAction: AyatanaMenuAction {
1005  model: menuModel
1006  index: menuIndex
1007  name: getExtendedProperty(extendedData, "xAyatanaPreviousAction", "")
1008  }
1009 
1010  playing: playAction.state === "Playing"
1011  canPlay: playAction.valid
1012  canGoNext: nextAction.valid
1013  canGoPrevious: previousAction.valid
1014  enabled: menuData && menuData.sensitive || false
1015  highlightWhenPressed: false
1016 
1017  onPlay: {
1018  playAction.activate();
1019  }
1020  onNext: {
1021  nextAction.activate();
1022  }
1023  onPrevious: {
1024  previousAction.activate();
1025  }
1026  onMenuModelChanged: {
1027  loadAttributes();
1028  }
1029  onMenuIndexChanged: {
1030  loadAttributes();
1031  }
1032 
1033  function loadAttributes() {
1034  if (!menuModel || menuIndex == -1) return;
1035  menuModel.loadExtendedAttributes(modelIndex, {'x-ayatana-play-action': 'string',
1036  'x-ayatana-next-action': 'string',
1037  'x-ayatana-previous-action': 'string'});
1038  }
1039  }
1040  }
1041 
1042  Component {
1043  id: transferMenu
1044 
1045  Menus.TransferMenu {
1046  objectName: "transferMenu"
1047  id: transfer
1048  property QtObject menuData: null
1049  property var menuModel: menuFactory.menuModel
1050  property int menuIndex: -1
1051  property var extendedData: menuData && menuData.ext || undefined
1052  property var uid: getExtendedProperty(extendedData, "xAyatanaUid", undefined)
1053 
1054  text: menuData && menuData.label || ""
1055  iconSource: menuData && menuData.icon || "image://theme/transfer-none"
1056  maximum: 1.0
1057  enabled: menuData && menuData.sensitive || false
1058  highlightWhenPressed: false
1059  removable: true
1060  confirmRemoval: true
1061 
1062  QDBusActionGroup {
1063  id: actionGroup
1064  busType: 1
1065  busName: menuFactory.rootModel.busName
1066  objectPath: menuFactory.rootModel.actions["indicator"]
1067 
1068  property var activateAction: action("activate-transfer")
1069  property var cancelAction: action("cancel-transfer")
1070  property var transferStateAction: uid !== undefined ? action("transfer-state."+uid) : null
1071 
1072  Component.onCompleted: actionGroup.start()
1073  }
1074 
1075  property var transferState: {
1076  if (actionGroup.transferStateAction === null) return undefined;
1077  return actionGroup.transferStateAction.valid ? actionGroup.transferStateAction.state : undefined
1078  }
1079 
1080  property var runningState : transferState !== undefined ? transferState["state"] : undefined
1081  property var secondsLeft : transferState !== undefined ? transferState["seconds-left"] : undefined
1082 
1083  active: runningState !== undefined && runningState !== Menus.TransferState.Finished
1084  progress: transferState !== undefined ? transferState["percent"] : 0.0
1085 
1086  // TODO - Should be in the SDK
1087  property var timeRemaining: {
1088  if (secondsLeft === undefined) return undefined;
1089 
1090  var remaining = "";
1091  var hours = Math.floor(secondsLeft / (60 * 60));
1092  var minutes = Math.floor(secondsLeft / 60) % 60;
1093  var seconds = secondsLeft % 60;
1094  if (hours > 0) {
1095  remaining += i18n.tr("%1 hour", "%1 hours", hours).arg(hours)
1096  }
1097  if (minutes > 0) {
1098  if (remaining != "") remaining += ", ";
1099  remaining += i18n.tr("%1 minute", "%1 minutes", minutes).arg(minutes)
1100  }
1101  // don't include seconds if hours > 0
1102  if (hours == 0 && minutes < 5 && seconds > 0) {
1103  if (remaining != "") remaining += ", ";
1104  remaining += i18n.tr("%1 second", "%1 seconds", seconds).arg(seconds)
1105  }
1106  if (remaining == "")
1107  remaining = i18n.tr("0 seconds");
1108  // Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
1109  return i18n.tr("%1 remaining").arg(remaining);
1110  }
1111 
1112  stateText: {
1113  switch (runningState) {
1114  case Menus.TransferState.Queued:
1115  return i18n.tr("In queue…");
1116  case Menus.TransferState.Hashing:
1117  case Menus.TransferState.Processing:
1118  case Menus.TransferState.Running:
1119  return timeRemaining === undefined ? i18n.tr("Downloading") : timeRemaining;
1120  case Menus.TransferState.Paused:
1121  return i18n.tr("Paused, tap to resume");
1122  case Menus.TransferState.Canceled:
1123  return i18n.tr("Canceled");
1124  case Menus.TransferState.Finished:
1125  return i18n.tr("Finished");
1126  case Menus.TransferState.Error:
1127  return i18n.tr("Failed, tap to retry");
1128  }
1129  return "";
1130  }
1131 
1132  onMenuModelChanged: {
1133  loadAttributes();
1134  }
1135  onMenuIndexChanged: {
1136  loadAttributes();
1137  }
1138  onTriggered: {
1139  actionGroup.activateAction.activate(uid);
1140  }
1141  onItemRemoved: {
1142  actionGroup.cancelAction.activate(uid);
1143  }
1144 
1145  function loadAttributes() {
1146  if (!menuModel || menuIndex == -1) return;
1147  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-uid': 'string'});
1148  }
1149  }
1150  }
1151 
1152  Component {
1153  id: buttonSectionMenu;
1154 
1155  Menus.ButtonMenu {
1156  objectName: "buttonSectionMenu"
1157  property QtObject menuData: null
1158  property var menuModel: menuFactory.menuModel
1159  property int menuIndex: -1
1160  property var extendedData: menuData && menuData.ext || undefined
1161 
1162  iconSource: menuData && menuData.icon || ""
1163  enabled: menuData && menuData.sensitive || false
1164  highlightWhenPressed: false
1165  text: menuData && menuData.label || ""
1166  foregroundColor: theme.palette.normal.backgroundText
1167  buttonText: getExtendedProperty(extendedData, "xAyatanaExtraLabel", "")
1168 
1169  onMenuModelChanged: {
1170  loadAttributes();
1171  }
1172  onMenuIndexChanged: {
1173  loadAttributes();
1174  }
1175  function loadAttributes() {
1176  if (!menuModel || menuIndex == -1) return;
1177  menuModel.loadExtendedAttributes(menuIndex, {'x-ayatana-extra-label': 'string'});
1178  }
1179 
1180  onButtonClicked: menuModel.activate(menuIndex);
1181  }
1182  }
1183 
1184  function load(modelData) {
1185  var component = getComponentForIndicatorEntryAction(modelData.action)
1186  if (component !== undefined) {
1187  return component
1188  }
1189 
1190  component = getComponentForIndicatorEntryType(modelData.type)
1191  if (component !== undefined) {
1192  return component;
1193  }
1194 
1195  if (modelData.isCheck) {
1196  return checkableMenu;
1197  }
1198  if (modelData.isRadio) {
1199  return radioMenu;
1200  }
1201  if (modelData.isSeparator) {
1202  return separatorMenu;
1203  }
1204  if (modelData.action !== undefined && modelData.action.indexOf("settings") > -1) {
1205  // FIXME : At the moment, the indicators aren't using
1206  // org.ayatana.indicators.link for settings menu. Need to fudge it.
1207  return linkMenu;
1208  }
1209  return standardMenu;
1210  }
1211 }