added program menu

This commit is contained in:
Maxim Stewart 2020-05-10 05:52:52 -05:00
parent 4be5195a80
commit e6d90bd76e
27 changed files with 577 additions and 862 deletions

View File

@ -8,7 +8,8 @@ Pytop is a Python + Gtk+ application to have a custom desktop interface.
n/a n/a
# TODO # TODO
* Add a program menu dropdown button. (Can use much of Shellmen's code.) n/a
# Images # Images
![1 Pytop Desktop showing Home directory. ](images/pic1.png) ![1 Pytop Desktop showing Desktop directory. ](images/pic1.png)
![2 Pytop Desktop showing main menu. ](images/pic2.png)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 KiB

After

Width:  |  Height:  |  Size: 879 KiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

View File

@ -7,6 +7,26 @@
<mime-type>inode/directory</mime-type> <mime-type>inode/directory</mime-type>
</mime-types> </mime-types>
</object> </object>
<object class="GtkAdjustment" id="brushSizeProp">
<property name="lower">1</property>
<property name="upper">100</property>
<property name="value">1</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
<signal name="value-changed" handler="onBrushSizeChange" swapped="no"/>
</object>
<object class="GtkImage" id="createImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-new</property>
</object>
<object class="GtkImage" id="menuImage">
<property name="width_request">64</property>
<property name="height_request">64</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixbuf">start_menu_icons/start_menu_icon2_32x32.png</property>
</object>
<object class="GtkWindow" id="Window"> <object class="GtkWindow" id="Window">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="default_width">800</property> <property name="default_width">800</property>
@ -28,7 +48,22 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
<placeholder/> <object class="GtkButton" id="progMenu">
<property name="label" translatable="yes">Menu</property>
<property name="width_request">64</property>
<property name="height_request">64</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">menuImage</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="toggleProgramMenu" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<placeholder/> <placeholder/>
@ -252,14 +287,6 @@
</object> </object>
</child> </child>
</object> </object>
<object class="GtkAdjustment" id="brushSizeProp">
<property name="lower">1</property>
<property name="upper">100</property>
<property name="value">1</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
<signal name="value-changed" handler="onBrushSizeChange" swapped="no"/>
</object>
<object class="GtkPopover" id="systemStats"> <object class="GtkPopover" id="systemStats">
<property name="width_request">500</property> <property name="width_request">500</property>
<property name="height_request">0</property> <property name="height_request">0</property>
@ -314,10 +341,255 @@
</object> </object>
</child> </child>
</object> </object>
<object class="GtkImage" id="createImage"> <object class="GtkApplicationWindow" id="menuWindow">
<property name="visible">True</property> <property name="width_request">600</property>
<property name="height_request">400</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="stock">gtk-new</property> <property name="border_width">4</property>
<property name="window_position">mouse</property>
<property name="destroy_with_parent">True</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<property name="deletable">False</property>
<property name="gravity">center</property>
<property name="has_resize_grip">True</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchEntry" id="searchProgramsEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="searchForEntry" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Accessories</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Multimedia</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Graphics</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Game</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Office</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Development</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Internet</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Settings</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">System</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Wine</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Other</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="setListGroup" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">10</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="programListBttns">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object> </object>
<object class="GtkImage" id="trashImage"> <object class="GtkImage" id="trashImage">
<property name="visible">True</property> <property name="visible">True</property>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,17 +1,19 @@
# Python imports # Python imports
from datetime import datetime from datetime import datetime
import os
# Gtk imports # Gtk imports
# Application imports # Application imports
from .mixins import CPUDrawMixin, TaskbarMixin, GridMixin from .mixins import CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin
from widgets import Grid from widgets import Grid
from widgets import Icon
from utils import FileHandler from utils import FileHandler
class Signals(CPUDrawMixin, TaskbarMixin, GridMixin): class Signals(CPUDrawMixin, MainMenuMixin, TaskbarMixin, GridMixin):
def __init__(self, settings): def __init__(self, settings):
self.settings = settings self.settings = settings
self.builder = self.settings.returnBuilder() self.builder = self.settings.returnBuilder()
@ -69,3 +71,18 @@ class Signals(CPUDrawMixin, TaskbarMixin, GridMixin):
selectDirDialog.add_filter(filefilter) selectDirDialog.add_filter(filefilter)
selectDirDialog.set_filename(self.currentPath) selectDirDialog.set_filename(self.currentPath)
self.setNewDirectory(selectDirDialog) self.setNewDirectory(selectDirDialog)
# Program Menu Parts
self.menuWindow = self.builder.get_object("menuWindow")
self.menuWindow.set_keep_above(True);
self.iconFactory = Icon(self.settings)
self.grpDefault = "Accessories"
self.progGroup = self.grpDefault
HOME_APPS = os.path.expanduser('~') + "/.local/share/applications/"
paths = ["/usr/share/applications/", HOME_APPS]
self.menuData = self.getDesktopFilesInfo(paths)
self.desktopObjs = []
self.getSubgroup()
self.generateListView()

View File

@ -6,7 +6,7 @@
class GridMixin: class GridMixin:
# different from the grid widget class' # Calls the Grid widget class' method
def setNewDirectory(self, widget, data=None): def setNewDirectory(self, widget, data=None):
newPath = widget.get_filename() newPath = widget.get_filename()
self.gridClss.setNewDirectory(newPath) self.gridClss.setNewDirectory(newPath)

View File

@ -0,0 +1,248 @@
# Python imports
import subprocess
import threading
import os
import json
from os.path import isdir, isfile, join
from os import listdir
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk as gtk
from gi.repository import GLib as glib
from xdg.DesktopEntry import DesktopEntry
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
return wrapper
class MainMenuMixin:
def toggleProgramMenu(self, widget):
pos = self.menuWindow.get_position()
posX = pos[0] + 32
posY = pos[1] + 72
if self.menuWindow.get_visible() == False:
self.menuWindow.move(posX, posY)
glib.idle_add(self.menuWindow.show_all)
else:
glib.idle_add(self.menuWindow.hide)
def setListGroup(self, widget):
self.progGroup = widget.get_label().strip()
self.getSubgroup(self.progGroup)
self.generateListView()
def searchForEntry(self, widget, data=None):
self.progGroup = "[ Search ]"
query = widget.get_text().strip()
if not query:
self.progGroup = self.grpDefault
self.getSubgroup()
self.generateListView()
return
self.getSubgroup(query)
self.generateListView()
@threaded
def generateListView(self):
widget = self.builder.get_object("programListBttns")
# Should have this as a useful method...But, I don't want to import Glib everywhere
children = widget.get_children()
for child in children:
glib.idle_add(widget.remove, (child))
for obj in self.desktopObjs:
title = obj[0]
dirPath = obj[1]
image = self.iconFactory.parseDesktopFiles(dirPath) # .get_pixbuf()
self.addToProgramListView(widget, title, image)
@threaded
def addToProgramListView(self, widget, title, icon):
button = gtk.Button(label=title)
button.set_image(icon)
button.connect("clicked", self.executeProgram)
button.show_all()
glib.idle_add(widget.add, (button))
def executeProgram(self, widget):
"""
Need to refactor and pull out the sub loop that is used in both cases...
"""
entry = widget.get_label().strip()
group = self.progGroup
parts = entry.split("||")
program = parts[0].strip()
comment = parts[1].strip()
if "[ Search ]" in group:
gkeys = self.menuData.keys()
for gkey in gkeys:
for opt in self.menuData[gkey]:
if program in opt["title"]:
keys = opt.keys()
if comment in opt["comment"] or comment in opt["fileName"]:
DEVNULL = open(os.devnull, 'w')
execFailed = False
try:
command = opt["tryExec"].split("%")[0]
# self.logger.debug(command)
subprocess.Popen(command.split(), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL)
break
except Exception as e:
execFailed = True
if execFailed:
try:
if "exec" in keys and len(opt["exec"]):
command = opt["exec"].split("%")[0]
# self.logger.debug(command)
subprocess.Popen(command.split(), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL)
break
except Exception as e:
print( repr(e) )
# self.logger.debug(e)
else:
for opt in self.menuData[group]:
if program in opt["title"]:
keys = opt.keys()
if comment in opt["comment"] or comment in opt["fileName"]:
DEVNULL = open(os.devnull, 'w')
execFailed = False
try:
command = opt["tryExec"].split("%")[0]
# self.logger.debug(command)
subprocess.Popen(command.split(), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL)
except Exception as e:
execFailed = True
if execFailed:
try:
if "exec" in keys and len(opt["exec"]):
command = opt["exec"].split("%")[0]
# self.logger.debug(command)
subprocess.Popen(command.split(), start_new_session=True, stdout=DEVNULL, stderr=DEVNULL)
except Exception as e:
print( repr(e) )
# self.logger.debug(e)
# Supoport methods
def getDesktopFilesInfo(self, paths):
menuObjs = {
"Accessories": [],
"Multimedia": [],
"Graphics": [],
"Game": [],
"Office": [],
"Development": [],
"Internet": [],
"Settings": [],
"System": [],
"Wine": [],
"Other": []
}
for path in paths:
for f in listdir(path):
fPath = path + f
flags = ["mimeinfo.cache", "defaults.list"]
if not f in flags and isfile(fPath):
xdgObj = DesktopEntry(fPath)
title = xdgObj.getName()
groups = xdgObj.getCategories()
comment = xdgObj.getComment()
icon = xdgObj.getIcon()
mainExec = xdgObj.getExec()
tryExec = xdgObj.getTryExec()
group = ""
if "Accessories" in groups or "Utility" in groups:
group = "Accessories"
elif "Multimedia" in groups or "Video" in groups or "Audio" in groups:
group = "Multimedia"
elif "Development" in groups:
group = "Development"
elif "Game" in groups:
group = "Game"
elif "Internet" in groups or "Network" in groups:
group = "Internet"
elif "Graphics" in groups:
group = "Graphics"
elif "Office" in groups:
group = "Office"
elif "System" in groups:
group = "System"
elif "Settings" in groups:
group = "Settings"
elif "Wine" in groups:
group = "Wine"
else:
group = "Other"
menuObjs[group].append( {"title": title, "groups": groups,
"comment": comment, "exec": mainExec,
"tryExec": tryExec, "fileName": f,
"filePath": fPath, "icon": icon})
return menuObjs
def getSubgroup(self, query = ""):
"""
Need to refactor and pull out the sub logic that is used in both cases...
"""
group = self.progGroup
self.desktopObjs.clear()
if "[ Search ]" in group:
gkeys = self.menuData.keys()
for gkey in gkeys:
for opt in self.menuData[gkey]:
keys = opt.keys()
if "comment" in keys and len(opt["comment"]) > 0 :
if query.lower() in opt["comment"].lower():
title = opt["title"] + " || " + opt["comment"]
fPath = opt["filePath"]
self.desktopObjs.append([title, fPath])
continue
if query.lower() in opt["title"].lower() or \
query.lower() in opt["fileName"].lower():
title = opt["title"] + " || " + opt["fileName"].replace(".desktop", "")
fPath = opt["filePath"]
self.desktopObjs.append([title, fPath])
else:
for opt in self.menuData[group]:
keys = opt.keys()
if "comment" in keys and len(opt["comment"]) > 0 :
title = opt["title"] + " || " + opt["comment"]
fPath = opt["filePath"]
self.desktopObjs.append([title, fPath])
else:
title = opt["title"] + " || " + opt["fileName"].replace(".desktop", "")
fPath = opt["filePath"]
self.desktopObjs.append([title, fPath])
return self.desktopObjs

View File

@ -1,3 +1,4 @@
from .MainMenuMixin import MainMenuMixin
from .TaskbarMixin import TaskbarMixin from .TaskbarMixin import TaskbarMixin
from .CPUDrawMixin import CPUDrawMixin from .CPUDrawMixin import CPUDrawMixin
from .GridMixin import GridMixin from .GridMixin import GridMixin

View File

@ -46,7 +46,6 @@ class Grid:
self.grid.set_model(self.store) self.grid.set_model(self.store)
self.grid.set_pixbuf_column(0) self.grid.set_pixbuf_column(0)
self.grid.set_text_column(1) self.grid.set_text_column(1)
print("Grid generated...")
self.grid.connect("item-activated", self.iconDblLeftClick) self.grid.connect("item-activated", self.iconDblLeftClick)
self.grid.connect("button_release_event", self.iconSingleClick, (self.grid,)) self.grid.connect("button_release_event", self.iconSingleClick, (self.grid,))

View File

@ -14,7 +14,7 @@ from xdg.DesktopEntry import DesktopEntry
# Application imports # Application imports
from .icon_manager import easybuttons
@ -113,15 +113,6 @@ class Icon:
execStr = xdgObj.getExec() execStr = xdgObj.getExec()
parts = execStr.split("steam://rungameid/") parts = execStr.split("steam://rungameid/")
id = parts[len(parts) - 1] id = parts[len(parts) - 1]
# NOTE: Can try this logic instead...
# if command exists use it instead of header image
# if "steamcmd app_info_print id":
# proc = subprocess.Popen(["steamcmd", "app_info_print", id])
# proc.wait()
# else:
# use the bottom logic
imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg" imageLink = "https://steamcdn-a.akamaihd.net/steam/apps/" + id + "/header.jpg"
proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink]) proc = subprocess.Popen(["wget", "-O", hashImgPth, imageLink])
proc.wait() proc.wait()
@ -131,14 +122,13 @@ class Icon:
elif os.path.exists(icon): elif os.path.exists(icon):
return self.createScaledImage(icon, self.systemIconImageWH) return self.createScaledImage(icon, self.systemIconImageWH)
else: else:
# return easybuttons.IconManager().getIcon(icon, 64) iconsDirs = ["/usr/share/pixmaps", self.usrHome + "/.icons", "/usr/share/icons" ,]
iconsDirs = "/usr/share/icons" altIconPath = ""
for (dirpath, dirnames, filenames) in os.walk(iconsDirs):
for file in filenames: for iconsDir in iconsDirs:
appNM = "application-x-" + icon altIconPath = self.traverseIconsFolder(iconsDir, icon)
if appNM in file: if altIconPath is not "":
altIconPath = dirpath + "/" + file break
break
return self.createScaledImage(altIconPath, self.systemIconImageWH) return self.createScaledImage(altIconPath, self.systemIconImageWH)
except Exception as e: except Exception as e:
@ -147,6 +137,19 @@ class Icon:
return None return None
def traverseIconsFolder(self, path, icon):
altIconPath = ""
for (dirpath, dirnames, filenames) in os.walk(path):
for file in filenames:
appNM = "application-x-" + icon
if icon in file or appNM in file:
altIconPath = dirpath + "/" + file
break
return altIconPath
def getSystemThumbnail(self, filename, size): def getSystemThumbnail(self, filename, size):
try: try:
if os.path.exists(filename): if os.path.exists(filename):

View File

@ -1,3 +0,0 @@
from . import easybuttons
from . import execute
from . import filemonitor

View File

@ -1,613 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007-2014 Clement Lefebvre <root@linuxmint.com>
# Copyright (C) 2015 Martin Wimpress <code@ubuntu-mate.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
import os
import re
import shutil
import xdg.DesktopEntry
import xdg.Menu
import gi
gi.require_version('Gtk', '3.0')
from .execute import *
from .filemonitor import monitor as filemonitor
from gi.repository import Gtk, Gdk, GLib
from gi.repository import Pango
from gi.repository import GObject
class IconManager(GObject.GObject):
__gsignals__ = {
"changed" : (GObject.SignalFlags.RUN_LAST, None, () )
}
def __init__( self ):
GObject.GObject.__init__( self )
self.icons = { }
self.count = 0
# Some apps don't put a default icon in the default theme folder, so we will search all themes
def createTheme( d ):
theme = Gtk.IconTheme()
theme.set_custom_theme( d )
return theme
# This takes to much time and there are only a very few applications that use icons from different themes
#self.themes = map( createTheme, [ d for d in os.listdir( "/usr/share/icons" ) if os.path.isdir( os.path.join( "/usr/share/icons", d ) ) ] )
self.defaultTheme = Gtk.IconTheme.get_default()
# Setup and clean up the temp icon dir
configDir = GLib.get_user_config_dir()
self.iconDir = os.path.join(configDir, "mate-menu")
if not os.path.exists(self.iconDir):
os.makedirs(self.iconDir)
# Skip over files and dirs belonging to the applications plugin
contents = frozenset(os.listdir(self.iconDir)) - frozenset(('applications', 'applications.list'))
for fn in contents:
if os.path.isfile(os.path.join(self.iconDir, fn)):
print("Removing file : " + os.path.join(self.iconDir, fn))
os.remove(os.path.join(self.iconDir, fn))
else:
print(os.path.join(self.iconDir, fn) + " is not a file, skipping delete.")
self.defaultTheme.append_search_path(self.iconDir)
# Themes with the same content as the default them aren't needed
#self.themes = [ theme for theme in self.themes if theme.list_icons() != defaultTheme.list_icons() ]
self.themes = [ self.defaultTheme ]
# Listen for changes in the themes
for theme in self.themes:
theme.connect("changed", self.themeChanged )
def getIcon( self, iconName, iconSize ):
if not iconName:
return None
try:
iconFileName = ""
realIconName = ""
needTempFile = False
#[ iconWidth, iconHeight ] = self.getIconSize( iconSize )
if iconSize <= 0:
return None
elif os.path.isabs( iconName ):
iconFileName = iconName
needTempFile = True
else:
if iconName[-4:] in [".png", ".xpm", ".svg", ".gif"]:
realIconName = iconName[:-4]
else:
realIconName = iconName
if iconFileName and needTempFile and os.path.exists( iconFileName ):
tmpIconName = iconFileName.replace("/", "-")
realIconName = tmpIconName[:-4]
if not os.path.exists(os.path.join(self.iconDir, tmpIconName)):
shutil.copyfile(iconFileName, os.path.join(self.iconDir, tmpIconName))
self.defaultTheme.append_search_path(self.iconDir)
image = Gtk.Image()
icon_found = False
for theme in self.themes:
if theme.lookup_icon( realIconName, 0, Gtk.IconLookupFlags.FORCE_REGULAR ):
icon_found = True
break
if icon_found:
image.set_from_icon_name(realIconName, Gtk.IconSize.DND)
image.set_pixel_size(iconSize)
else:
image = None
return image
except Exception as e:
print("Exception " + e.__class__.__name__ + ": " + e.message)
return None
def themeChanged( self, theme ):
self.emit( "changed" )
GObject.type_register(IconManager)
class easyButton( Gtk.Button ):
def __init__( self, iconName, iconSize, labels = None, buttonWidth = -1, buttonHeight = -1 ):
GObject.GObject.__init__( self )
self.connections = [ ]
self.iconName = iconName
self.iconSize = iconSize
self.showIcon = True
self.set_relief( Gtk.ReliefStyle.NONE )
self.set_size_request( buttonWidth, buttonHeight )
HBox1 = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL )
HBox1.set_valign(Gtk.Align.CENTER)
HBox1.set_hexpand(True)
self.labelBox = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=2 )
self.buttonImage = Gtk.Image()
icon = self.getIcon( self.iconSize )
if icon:
self.buttonImage = icon
else:
#[ iW, iH ] = iconManager.getIconSize( self.iconSize )
self.buttonImage.set_size_request( self.iconSize, self.iconSize )
self.image_box = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL )
self.image_box.pack_start(self.buttonImage, False, False, 5)
self.image_box.show_all()
HBox1.pack_start( self.image_box, False, False, 0 )
if labels:
for label in labels:
if isinstance( label, str ):
self.addLabel( label )
elif isinstance( label, list ):
self.addLabel( label[0], label[1] )
self.labelBox.show()
HBox1.pack_start( self.labelBox , True, True, 0)
HBox1.show()
self.add( HBox1 )
self.set_events(Gdk.EventMask.POINTER_MOTION_MASK)
self.connectSelf( "motion-notify-event", self.onMotion )
self.connectSelf( "enter-notify-event", self.onEnter )
self.connectSelf( "focus-in-event", self.onFocusIn )
self.connectSelf( "focus-out-event", self.onFocusOut )
self.connectSelf( "destroy", self.onDestroy )
self.connect( "released", self.onRelease )
# Reload icons when the theme changed
self.themeChangedHandlerId = iconManager.connect("changed", self.themeChanged )
def connectSelf( self, event, callback ):
self.connections.append( self.connect( event, callback ) )
def onMotion( self, widget, event ):
# Only grab if mouse is actually hovering
if self.mouse_entered:
self.grab_focus()
self.mouse_entered = False
def onEnter( self, widget, event ):
# Prevent false "enter" notifications by determining
# whether the mouse is actually hovering on the button.
self.mouse_entered = True
def onFocusIn( self, widget, event ):
self.set_state_flags( Gtk.StateFlags.PRELIGHT, False )
def onFocusOut( self, widget, event ):
self.unset_state_flags( Gtk.StateFlags.PRELIGHT )
def onRelease( self, widget ):
widget.get_style_context().set_state( Gtk.StateFlags.NORMAL )
def onDestroy( self, widget ):
self.buttonImage.clear()
iconManager.disconnect( self.themeChangedHandlerId )
for connection in self.connections:
self.disconnect( connection )
del self.connections
def addLabel( self, text, styles = None ):
label = Gtk.Label()
if "<b>" in text or "<span" in text:
label.set_markup(text.replace('&', '&amp;')) # don't remove our pango
else:
label.set_text(text)
if styles:
labelStyle = Pango.AttrList()
for attr in styles:
labelStyle.insert( attr )
label.set_attributes( labelStyle )
label.set_ellipsize( Pango.EllipsizeMode.END )
if (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION) >= (3, 16):
label.set_xalign(0.0)
label.set_yalign(1.0)
else:
label.set_alignment( 0.0, 1.0 )
label.set_max_width_chars(0)
label.show()
self.labelBox.pack_start( label , True, True, 0)
def getIcon ( self, iconSize ):
if not self.iconName:
return None
icon = iconManager.getIcon( self.iconName, iconSize )
if icon is None:
icon = iconManager.getIcon( "gtk-missing-image", iconSize )
return icon
def setIcon ( self, iconName ):
self.iconName = iconName
self.iconChanged()
# IconTheme changed, setup new button icons
def themeChanged( self, theme ):
self.iconChanged()
def iconChanged( self ):
icon = self.getIcon( self.iconSize )
self.buttonImage.destroy()
if icon:
self.buttonImage = icon
self.image_box.pack_start(self.buttonImage, False, False, 5)
self.image_box.show_all()
else:
#[iW, iH ] = iconManager.getIconSize( self.iconSize )
self.buttonImage.set_size_request( self.iconSize, self.iconSize )
def setIconSize( self, size ):
self.iconSize = size
icon = self.getIcon( self.iconSize )
self.buttonImage.destroy()
if icon:
self.buttonImage = icon
self.image_box.pack_start(self.buttonImage, False, False, 5)
self.image_box.show_all()
elif self.iconSize:
#[ iW, iH ] = iconManager.getIconSize( self.iconSize )
self.buttonImage.set_size_request( self.iconSize, self.iconSize )
class ApplicationLauncher( easyButton ):
def __init__( self, desktopFile, iconSize):
if isinstance( desktopFile, xdg.Menu.MenuEntry ):
desktopItem = desktopFile.DesktopEntry
desktopFile = desktopItem.filename
self.appDirs = desktop.desktopFile.AppDirs
elif isinstance( desktopFile, xdg.Menu.DesktopEntry ):
desktopItem = desktopFile
desktopFile = desktopItem.filename
self.appDirs = [ os.path.dirname( desktopItem.filename ) ]
else:
desktopItem = xdg.DesktopEntry.DesktopEntry( desktopFile )
self.appDirs = [ os.path.dirname( desktopFile ) ]
self.desktopFile = desktopFile
self.startupMonitorId = 0
self.relevance = 0
self.loadDesktopEntry( desktopItem )
self.desktopEntryMonitors = []
base = os.path.basename( self.desktopFile )
for dir in self.appDirs:
self.desktopEntryMonitors.append( filemonitor.addMonitor( os.path.join(dir, base) , self.desktopEntryFileChangedCallback ) )
easyButton.__init__( self, self.appIconName, iconSize )
self.setupLabels()
# Drag and Drop
self.connectSelf( "drag-data-get", self.dragDataGet )
targets = ( Gtk.TargetEntry.new( "text/plain", 0, 100 ), Gtk.TargetEntry.new( "text/uri-list", 0, 101 ) )
self.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY )
icon = self.getIcon( Gtk.IconSize.DND )
if icon:
iconName, s = icon.get_icon_name()
self.drag_source_set_icon_name( iconName )
self.connectSelf( "focus-in-event", self.onFocusIn )
self.connectSelf( "focus-out-event", self.onFocusOut )
self.connectSelf( "clicked", self.execute )
def loadDesktopEntry( self, desktopItem ):
try:
self.appName = desktopItem.getName()
self.appGenericName = desktopItem.getGenericName()
self.appComment = desktopItem.getComment()
self.appExec = desktopItem.getExec().replace('\\\\', '\\')
self.appIconName = desktopItem.getIcon()
self.appCategories = desktopItem.getCategories()
self.appMateDocPath = desktopItem.get( "X-MATE-DocPath" ) or ""
self.useTerminal = desktopItem.getTerminal()
self.appPath = desktopItem.getPath()
self.appName = self.appName.strip()
self.appGenericName = self.appGenericName.strip()
self.appComment = self.appComment.strip()
configPath = os.environ.get( "XDG_CONFIG_HOME",
os.path.join( os.environ["HOME"], ".config" ) )
basename = os.path.basename( self.desktopFile )
self.startupFilePath = os.path.join( configPath, "autostart", basename )
if self.startupMonitorId:
filemonitor.removeMonitor( self.startupMonitorId )
if os.path.exists (self.startupFilePath):
self.startupMonitorId = filemonitor.addMonitor( self.startupFilePath, self.startupFileChanged )
except Exception as e:
print(e)
self.appName = ""
self.appGenericName = ""
self.appComment = ""
self.appExec = ""
self.appIconName = ""
self.appCategories = ""
self.appDocPath = ""
self.startupMonitorId = 0
def onFocusIn( self, widget, event ):
super(ApplicationLauncher, self).onFocusIn( widget, event )
self.set_relief( Gtk.ReliefStyle.HALF )
def onFocusOut( self, widget, event ):
super(ApplicationLauncher, self).onFocusOut( widget, event )
self.set_relief( Gtk.ReliefStyle.NONE )
def setupLabels( self ):
self.addLabel( self.appName )
def filterText( self, text ):
keywords = text.lower().split()
self.relevance = 0
appName = self.appName.lower()
appGenericName = self.appGenericName.lower()
appComment = self.appComment.lower()
appExec = self.appExec.lower()
for keyword in keywords:
keyw = keyword
# Hide if the term does not match
if keyw != "" and appName.find( keyw ) == -1 and appGenericName.find( keyw ) == -1 and appComment.find( keyw ) == -1 and appExec.find( keyw ) == -1:
self.hide()
return False
# Give better ranking to the actual app name
if appName == keyw:
self.relevance += 32
elif appName.find( keyw ) == 0:
self.relevance += 16
elif appName.find( keyw ) != -1:
self.relevance += 8
if appExec.find( keyw ) != -1:
self.relevance += 4
if appComment.find( keyw ) != -1:
self.relevance += 2
if appGenericName.find( keyw ) != -1:
self.relevance += 1
self.show()
return True
def getTooltip( self ):
tooltip = self.appName
if self.appComment != "" and self.appComment != self.appName:
tooltip = tooltip + "\n" + self.appComment
return tooltip
def dragDataGet( self, widget, context, selection, targetType, eventTime ):
if targetType == 100: # text/plain
selection.set_text( "'" + self.desktopFile + "'", -1 )
elif targetType == 101: # text/uri-list
if self.desktopFile[0:7] == "file://":
selection.set_uris( [ self.desktopFile ] )
else:
selection.set_uris( [ "file://" + self.desktopFile ] )
def execute( self, *args ):
def pathExists(file):
if os.path.exists(file):
return True
for path in os.environ["PATH"].split(os.pathsep):
if os.path.exists(os.path.join(path, file)):
return True
if self.appExec:
if self.useTerminal:
if pathExists("mate-terminal"):
cmd = "mate-terminal -e \"" + self.appExec + "\""
elif pathExists("x-terminal-emulator"):
cmd = "x-terminal-emulator -e \"" + self.appExec + "\""
else:
cmd = "xterm -e \"" + self.appExec + "\""
Execute(cmd, self.appPath)
else:
Execute(self.appExec, self.appPath)
# IconTheme changed, setup new icons for button and drag 'n drop
def iconChanged( self ):
easyButton.iconChanged( self )
icon = self.getIcon( Gtk.IconSize.DND )
if icon:
iconName, size = icon.get_icon_name()
self.drag_source_set_icon_name( iconName )
def startupFileChanged( self, *args ):
self.inStartup = os.path.exists( self.startupFilePath )
def removeFromStartup( self ):
if os.path.exists( self.startupFilePath ):
os.remove( self.startupFilePath )
def addToFavourites( self ):
configPath = os.environ.get( "XDG_CONFIG_HOME",
os.path.join( os.environ["HOME"], ".config" ) )
favouritesDir = os.path.join( configPath, "mate-menu", "applications" );
if not os.path.exists( favouritesDir ):
os.makedirs( favouritesDir )
shutil.copyfile( self.desktopFile, self.favouritesFilePath )
def removeFromFavourites( self ):
if os.path.exists( self.favouritesFilePath ):
os.remove( self.favouritesFilePath )
def isInStartup( self ):
#return self.inStartup
return os.path.exists( self.startupFilePath )
def onDestroy( self, widget ):
easyButton.onDestroy( self, widget )
if self.startupMonitorId:
filemonitor.removeMonitor( self.startupMonitorId )
for id in self.desktopEntryMonitors:
filemonitor.removeMonitor( id )
def desktopEntryFileChangedCallback (self):
GLib.timeout_add(200, self.onDesktopEntryFileChanged)
def onDesktopEntryFileChanged( self ):
exists = False
base = os.path.basename( self.desktopFile )
for dir in self.appDirs:
if os.path.exists( os.path.join( dir, base ) ):
# print(os.path.join( dir, base ), self.desktopFile)
self.loadDesktopEntry( xdg.DesktopEntry.DesktopEntry( os.path.join( dir, base ) ) )
for child in self.labelBox:
child.destroy()
self.iconName = self.appIconName
self.setupLabels()
self.iconChanged()
exists = True
break
if not exists:
# FIXME: What to do in this case?
self.destroy()
return False
class MenuApplicationLauncher( ApplicationLauncher ):
def __init__( self, desktopFile, iconSize, category, showComment, highlight=False ):
self.showComment = showComment
self.appCategory = category
self.highlight = highlight
ApplicationLauncher.__init__( self, desktopFile, iconSize )
def filterCategory( self, category ):
if self.appCategory == category or category == "":
self.show()
else:
self.hide()
def setupLabels( self ):
appName = self.appName
appComment = self.appComment
if self.highlight:
try:
#color = self.labelBox.get_style_context().get_color( Gtk.StateFlags.SELECTED ).to_string()
#if len(color) > 0 and color[0] == "#":
#appName = "<span foreground=\"%s\"><b>%s</b></span>" % (color, appName);
#appComment = "<span foreground=\"%s\"><b>%s</b></span>" % (color, appComment);
#appName = "<b>%s</b>" % (appName);
#appComment = "<b>%s</b>" % (appComment);
#else:
#appName = "<b>%s</b>" % (appName);
#appComment = "<b>%s</b>" % (appComment);
appName = "<b>%s</b>" % (appName);
appComment = "<b>%s</b>" % (appComment);
except Exception as detail:
print(detail)
pass
if self.showComment and self.appComment != "":
if self.iconSize <= 2:
self.addLabel( '<span size="small">%s</span>' % appName)
self.addLabel( '<span size="x-small">%s</span>' % appComment)
else:
self.addLabel( appName )
self.addLabel( '<span size="small">%s</span>' % appComment)
else:
self.addLabel( appName )
def execute( self, *args ):
self.highlight = False
for child in self.labelBox:
child.destroy()
self.setupLabels()
return super(MenuApplicationLauncher, self).execute(*args)
def setShowComment( self, showComment ):
self.showComment = showComment
for child in self.labelBox:
child.destroy()
self.setupLabels()
class FavApplicationLauncher( ApplicationLauncher ):
def __init__( self, desktopFile, iconSize, swapGeneric = False ):
self.swapGeneric = swapGeneric
ApplicationLauncher.__init__( self, desktopFile, iconSize )
def setupLabels( self ):
if self.appGenericName:
if self.swapGeneric:
self.addLabel( '<span weight="bold">%s</span>' % self.appName )
self.addLabel( self.appGenericName )
else:
self.addLabel( '<span weight="bold">%s</span>' % self.appGenericName )
self.addLabel( self.appName )
else:
self.addLabel( '<span weight="bold">%s</span>' % self.appName )
if self.appComment != "":
self.addLabel( self.appComment )
else:
self.addLabel ( "" )
def setSwapGeneric( self, swapGeneric ):
self.swapGeneric = swapGeneric
for child in self.labelBox:
child.destroy()
self.setupLabels()
class CategoryButton( easyButton ):
def __init__( self, iconName, iconSize, labels , f ):
easyButton.__init__( self, iconName, iconSize, labels )
self.filter = f
self.set_focus_on_click(False)
iconManager = IconManager()

View File

@ -1,74 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007-2014 Clement Lefebvre <root@linuxmint.com>
# Copyright (C) 2015 Martin Wimpress <code@ubuntu-mate.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
import os
import shlex
import subprocess
def RemoveArgs(Execline):
NewExecline = []
Specials=["\"%c\"", "%f","%F","%u","%U","%d","%D","%n","%N","%i","%c","%k","%v","%m","%M", "-caption", "/bin/sh", "sh", "-c", "STARTED_FROM_MENU=yes"]
for elem in Execline:
elem = elem.replace("'","")
elem = elem.replace("\"", "")
if elem not in Specials:
print(elem)
NewExecline.append(elem)
return NewExecline
# Actually execute the command
def ExecuteCommand(cmd , commandCwd=None):
if not commandCwd:
cwd = os.path.expanduser( "~" );
else:
tmpCwd = os.path.expanduser( commandCwd );
if (os.path.exists(tmpCwd)):
cwd = tmpCwd
if isinstance( cmd, str ):
if (cmd.find("/home/") >= 0) or (cmd.find("xdg-su") >= 0) or (cmd.find("\"") >= 0):
print("running manually...")
try:
os.chdir(cwd)
subprocess.Popen(shlex.split(cmd))
return True
except Exception as detail:
print(detail)
return False
cmd = cmd.split()
cmd = RemoveArgs(cmd)
try:
os.chdir( cwd )
string = ' '.join(cmd)
subprocess.Popen(shlex.split(string))
return True
except Exception as detail:
print(detail)
return False
# Execute cmd using the double fork method
def Execute(cmd, commandCwd=None):
child_pid = os.fork()
if child_pid == 0:
ExecuteCommand(cmd, commandCwd)
os._exit(0)
else:
os.wait()

View File

@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007-2014 Clement Lefebvre <root@linuxmint.com>
# Copyright (C) 2015 Martin Wimpress <code@ubuntu-mate.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
import os
import os.path
import threading
import time
from gi.repository import GLib
try:
import pyinotify
hasInotify = True
except ImportError:
hasInotify = False
if hasInotify:
class FileMonitor(object):
def __init__( self ):
self.monitorId = 0
self.wm = pyinotify.WatchManager()
self.wdds = {}
self.callbacks = {}
self.notifier = pyinotify.ThreadedNotifier(self.wm, self.fileChanged)
self.notifier.setDaemon( True )
self.notifier.start()
def addMonitor( self, filename, callback, args = None ):
try:
mask = pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY
mId = self.wm.add_watch( filename, mask, rec = True)[filename]
if mId >= 0:
self.callbacks[mId] = ( callback, args )
except Exception as detail:
mId = 0
return mId
def removeMonitor( self, monitorId ):
if monitorId in self.callbacks:
self.wm.rm_watch( monitorId )
del self.callbacks[monitorId]
def fileChanged(self, event ):
if event.wd in self.callbacks:
callback = self.callbacks[event.wd]
if callback[1]:
GLib.idle_add( callback[0], callback[1] )
else:
GLib.idle_add( callback[0] )
else:
class _MonitoredFile( object ):
def __init__( self, filename, callback, monitorId, args ):
self.filename = filename
self.callback = callback
self.monitorId = monitorId
self.args = args
self.exists = os.path.exists( self.filename )
if self.exists:
self.mtime = os.stat( filename ).st_mtime
else:
self.mtime = 0
def hasChanged( self ):
if os.path.exists( self.filename ):
if not self.exists:
self.exists = True
self.mtime = os.stat( self.filename ).st_mtime
return True
else:
mtime = os.stat( self.filename ).st_mtime
if mtime != self.mtime:
self.mtime = mtime
return True
else:
if self.exists:
self.exists = False
return True
return False
class MonitorThread(threading.Thread):
def __init__(self, monitor):
threading.Thread.__init__ ( self )
self.monitor = monitor
def run(self):
while(1):
self.monitor.checkFiles()
time.sleep(1)
class FileMonitor(object):
def __init__( self ):
self.monitorId = 0
self.monitoredFiles = []
self.monitorThread = MonitorThread( self )
self.monitorThread.setDaemon( True )
self.monitorThread.start()
def addMonitor( self, filename, callback, args = None ):
self.monitorId += 1
self.monitoredFiles.append( _MonitoredFile( filename, callback, self.monitorId, args ) )
return self.monitorId
def removeMonitor( self, monitorId ):
for monitored in self.monitoredFiles:
if monitorId == monitored.monitorId:
self.monitoredFiles.remove( monitored )
break
def checkFiles( self ):
for monitored in self.monitoredFiles:
if monitored.hasChanged():
if monitored.args:
GLib.idle_add( monitored.callback, monitored.args )
else:
GLib.idle_add( monitored.callback )
monitor = FileMonitor()