main.py 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944
  1. #!/usr/bin/env python3
  2. # _*_ coding: utf-8 _*_
  3. """
  4. Wallpaper and colour manager for Sway, i3 and some other WMs, as a frontend to swaybg and feh
  5. Author: Piotr Miller & Contributors
  6. e-mail: nwg.piotr@gmail.com
  7. Website: http://nwg.pl
  8. Project: https://github.com/nwg-piotr/azote
  9. License: GPL3
  10. """
  11. import os
  12. import sys
  13. import subprocess
  14. import stat
  15. import gi
  16. import cairo
  17. from PIL import Image
  18. from azote import common
  19. # send2trash module may or may not be available
  20. try:
  21. from send2trash import send2trash
  22. common.env['send2trash'] = True
  23. except Exception:
  24. common.env['send2trash'] = False
  25. print('python-send2trash package not found - deleting pictures unavailable')
  26. from colorthief import ColorThief
  27. gi.require_version('Gtk', '3.0')
  28. from gi.repository import Gtk, GdkPixbuf, Gdk, GLib
  29. from gi.repository.GdkPixbuf import InterpType
  30. from azote.tools import set_env, hash_name, create_thumbnails, file_allowed, update_status_bar, flip_selected_wallpaper, \
  31. copy_backgrounds, create_pixbuf, split_selected_wallpaper, scale_and_crop, clear_thumbnails, current_display, \
  32. save_json, load_json
  33. from azote.color_tools import rgba_to_hex, hex_to_rgb, rgb_to_hex, rgb_to_rgba
  34. from azote.plugins import Alacritty, Xresources
  35. from azote.color_tools import WikiColours
  36. try:
  37. gi.require_version('AppIndicator3', '0.1')
  38. from gi.repository import AppIndicator3
  39. common.env['app_indicator'] = True
  40. except:
  41. common.env['app_indicator'] = False
  42. print('libappindicator-gtk3 package not found - tray icon unavailable')
  43. from azote.__about__ import __version__
  44. def get_files():
  45. try:
  46. inames = "-iname \"*."+"\" -o -iname \"*.".join(common.allowed_file_types)+"\""
  47. files = subprocess.check_output("find '%s' -mindepth 1 %s" %(common.settings.src_path, inames), shell=True).decode().split("\n")[:-1]
  48. file_names = [f.replace(common.settings.src_path+"/", "") for f in files]
  49. except FileNotFoundError:
  50. common.settings.src_path = os.getenv('HOME')
  51. file_names = [f for f in os.listdir(common.settings.src_path)
  52. if os.path.isfile(os.path.join(common.settings.src_path, f))]
  53. if common.settings.sorting == 'new':
  54. file_names.sort(reverse=True, key=lambda f: os.path.getmtime(os.path.join(common.settings.src_path, f)))
  55. elif common.settings.sorting == 'old':
  56. file_names.sort(key=lambda f: os.path.getmtime(os.path.join(common.settings.src_path, f)))
  57. elif common.settings.sorting == 'az':
  58. file_names.sort()
  59. elif common.settings.sorting == 'za':
  60. file_names.sort(reverse=True)
  61. return file_names
  62. class Preview(Gtk.ScrolledWindow):
  63. def __init__(self):
  64. super().__init__()
  65. self.set_border_width(10)
  66. self.set_propagate_natural_height(True)
  67. self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
  68. common.thumbnails_list = []
  69. self.grid = Gtk.FlowBox()
  70. self.grid.set_valign(Gtk.Align.START)
  71. # self.grid.set_max_children_per_line(30)
  72. self.grid.set_selection_mode(Gtk.SelectionMode.NONE)
  73. create_thumbnails(common.settings.src_path)
  74. self.files_dict = dict([(f, None) for f in os.listdir(common.settings.src_path)])
  75. src_pictures = get_files()
  76. for file in src_pictures:
  77. if file_allowed(file):
  78. thumbnail = Thumbnail(common.settings.src_path, file)
  79. common.thumbnails_list.append(thumbnail)
  80. self.grid.add(thumbnail)
  81. self.add(self.grid)
  82. def refresh(self, create_thumbs=True):
  83. self.files_dict = dict([(f, None) for f in os.listdir(common.settings.src_path)])
  84. if create_thumbs:
  85. create_thumbnails(common.settings.src_path)
  86. for thumbnail in common.thumbnails_list:
  87. self.grid.remove(thumbnail)
  88. thumbnail.destroy()
  89. src_pictures = get_files()
  90. for file in src_pictures:
  91. if file_allowed(file):
  92. thumbnail = Thumbnail(common.settings.src_path, file)
  93. common.thumbnails_list.append(thumbnail)
  94. self.grid.add(thumbnail)
  95. thumbnail.show_all()
  96. thumbnail.toolbar.hide()
  97. update_status_bar()
  98. class Thumbnail(Gtk.VBox):
  99. def __init__(self, folder, filename):
  100. super().__init__()
  101. self.toolbar = ImageToolbar(self)
  102. self.add(self.toolbar)
  103. self.image_button = Gtk.Button()
  104. self.image_button.set_property("name", "thumb-btn")
  105. self.image_button.set_always_show_image(True)
  106. self.folder = folder
  107. self.filename = filename
  108. self.source_path = os.path.join(folder, filename)
  109. self.img = Gtk.Image()
  110. self.thumb_file = "{}.png".format(os.path.join(common.thumb_dir, hash_name(self.source_path)))
  111. self.img.set_from_file(self.thumb_file)
  112. self.image_button.set_image(self.img)
  113. self.image_button.set_image_position(2) # TOP
  114. self.image_button.set_tooltip_text(common.lang['thumbnail_tooltip'])
  115. if len(filename) > 30:
  116. filename = '…{}'.format(filename[-28::])
  117. self.image_button.set_label(filename)
  118. self.selected = False
  119. # self.connect('clicked', self.on_button_press)
  120. self.image_button.connect('button-press-event', self.on_image_button_press)
  121. self.add(self.image_button)
  122. def on_image_button_press(self, button, event):
  123. self.select(button)
  124. if event.type == Gdk.EventType._2BUTTON_PRESS:
  125. on_thumb_double_click(button)
  126. if event.button == 3:
  127. show_image_menu(self)
  128. def on_menu_button_press(self, button):
  129. show_image_menu(self)
  130. def select(self, thumbnail):
  131. if common.split_button:
  132. common.split_button.set_sensitive(True)
  133. common.apply_to_all_button.set_sensitive(True)
  134. self.image_button.selected = True
  135. common.selected_wallpaper = self
  136. deselect_all()
  137. if common.settings.image_menu_button:
  138. self.toolbar.show_all()
  139. thumbnail.set_property("name", "thumb-btn-selected")
  140. with Image.open(self.source_path) as img:
  141. filename = self.filename
  142. if len(filename) > 30:
  143. filename = '…{}'.format(filename[-28::])
  144. common.selected_picture_label.set_text("{} ({} x {})".format(filename, img.size[0], img.size[1]))
  145. def deselect(self, thumbnail):
  146. self.selected = False
  147. self.toolbar.hide()
  148. thumbnail.image_button.set_property("name", "thumb-btn")
  149. def deselect_all():
  150. for thumbnail in common.thumbnails_list:
  151. thumbnail.deselect(thumbnail)
  152. class ImageToolbar(Gtk.HBox):
  153. def __init__(self, thumbnail):
  154. super().__init__()
  155. self.parent_thumbnail = thumbnail
  156. self.set_spacing(0)
  157. self.set_border_width(0)
  158. test_label = Gtk.Label()
  159. test_label.set_text('')
  160. self.pack_start(test_label, True, True, 0)
  161. self.menu_btn = Gtk.EventBox()
  162. img = Gtk.Image()
  163. img.set_from_file('images/icon_image_menu.svg')
  164. self.menu_btn.add(img)
  165. self.menu_btn.connect('button-press-event', self.on_menu_button_press)
  166. self.pack_start(self.menu_btn, False, False, 0)
  167. def on_menu_button_press(self, button, event):
  168. show_image_menu(self.parent_thumbnail, from_toolbar=True)
  169. class DisplayBox(Gtk.Box):
  170. """
  171. The box contains elements to preview certain displays and assign wallpapers to them
  172. """
  173. def __init__(self, name, width, height, path=None, thumb=None, xrandr_idx=None):
  174. super().__init__()
  175. self.set_orientation(Gtk.Orientation.VERTICAL)
  176. # Values to assigned to corresponding display when apply button pressed
  177. self.display_name = name
  178. self.wallpaper_path = path
  179. self.thumbnail_path = thumb
  180. self.mode = 'fill' if common.sway or common.env['wayland'] else 'scale'
  181. self.color = None
  182. self.xrandr_idx = xrandr_idx
  183. self.include = True
  184. if thumb and os.path.isfile(thumb):
  185. pixbuf = GdkPixbuf.Pixbuf.new_from_file(thumb)
  186. else:
  187. pixbuf = GdkPixbuf.Pixbuf.new_from_file('images/empty.png')
  188. pixbuf = pixbuf.scale_simple(common.settings.thumb_size[0], common.settings.thumb_size[1], InterpType.BILINEAR)
  189. self.img = Gtk.Image.new_from_pixbuf(pixbuf)
  190. self.select_button = Gtk.Button()
  191. self.select_button.set_always_show_image(True)
  192. self.select_button.set_label("{} ({} x {})".format(name, width, height)) # label on top: name (with x height)
  193. self.select_button.set_image(self.img) # preview of selected wallpaper
  194. self.select_button.set_image_position(3) # label on top, image below
  195. self.select_button.set_property("name", "display-btn") # to assign css style
  196. self.select_button.set_tooltip_text(common.lang['set_selected_wallpaper'])
  197. self.pack_start(self.select_button, False, False, 10)
  198. self.select_button.connect_after('clicked', self.on_select_button)
  199. # Combo box to choose a mode to use for the image
  200. mode_selector = Gtk.ListStore(str)
  201. if common.sway or common.env['wayland']:
  202. for mode in common.modes_swaybg:
  203. mode_selector.append([mode])
  204. else:
  205. for mode in common.modes_feh:
  206. mode_selector.append([mode])
  207. # Let's display the mode combo and the color button side-by-side in a vertical box
  208. options_box = Gtk.Box()
  209. options_box.set_spacing(15)
  210. options_box.set_border_width(0)
  211. options_box.set_orientation(Gtk.Orientation.HORIZONTAL)
  212. self.pack_start(options_box, True, False, 0)
  213. check_button = Gtk.CheckButton()
  214. check_button.set_active(self.include)
  215. check_button.set_tooltip_text(common.lang["include_when_splitting"])
  216. check_button.connect("toggled", self.switch_included)
  217. options_box.pack_start(check_button, False, False, 0)
  218. self.mode_combo = Gtk.ComboBox.new_with_model(mode_selector)
  219. self.mode_combo.set_active(2)
  220. self.mode_combo.connect("changed", self.on_mode_combo_changed)
  221. renderer_text = Gtk.CellRendererText()
  222. self.mode_combo.pack_start(renderer_text, True)
  223. self.mode_combo.add_attribute(renderer_text, "text", 0)
  224. self.mode_combo.set_tooltip_text(common.lang['display_mode'])
  225. options_box.add(self.mode_combo)
  226. if common.sway or common.env['wayland']:
  227. # Color button
  228. self.color_button = Gtk.ColorButton()
  229. color = Gdk.RGBA()
  230. color.red = 0.0
  231. color.green = 0.0
  232. color.blue = 0.0
  233. color.alpha = 1.0
  234. self.color_button.set_rgba(color)
  235. self.color_button.connect("color-set", self.on_color_chosen, self.color_button)
  236. self.color_button.set_tooltip_text(common.lang['background_color'])
  237. options_box.add(self.color_button)
  238. self.flip_button = Gtk.Button()
  239. self.flip_button.set_always_show_image(True)
  240. img = Gtk.Image()
  241. img.set_from_file('images/icon_flip.svg')
  242. self.flip_button.set_image(img)
  243. self.flip_button.set_tooltip_text(common.lang['flip_image'])
  244. self.flip_button.set_sensitive(False)
  245. self.flip_button.connect('clicked', self.on_flip_button)
  246. self.flip_button.set_tooltip_text(common.lang['flip_wallpaper_horizontally'])
  247. options_box.pack_start(self.flip_button, True, True, 0)
  248. def switch_included(self, ckb):
  249. self.include = ckb.get_active()
  250. def clear_color_selection(self):
  251. # If not on sway / swaybg, we have no color_button in UI
  252. if common.sway or common.env['wayland']:
  253. # clear color selection: image will be used
  254. color = Gdk.RGBA()
  255. color.red = 0.0
  256. color.green = 0.0
  257. color.blue = 0.0
  258. color.alpha = 1.0
  259. self.color_button.set_rgba(color)
  260. self.color = None
  261. def on_select_button(self, button):
  262. if common.selected_wallpaper:
  263. self.img.set_from_file(common.selected_wallpaper.thumb_file)
  264. self.wallpaper_path = common.selected_wallpaper.source_path
  265. self.thumbnail_path = common.selected_wallpaper.thumb_file
  266. button.set_property("name", "display-btn-selected")
  267. self.flip_button.set_sensitive(True)
  268. self.clear_color_selection()
  269. common.apply_button.set_sensitive(True)
  270. def on_mode_combo_changed(self, combo):
  271. tree_iter = combo.get_active_iter()
  272. if tree_iter is not None:
  273. model = combo.get_model()
  274. mode = model[tree_iter][0]
  275. self.mode = mode
  276. # If our backend is feh, not swaybg, we can not set mode for each wallpaper separately.
  277. # Let's copy the same selection to all displays.
  278. if not common.sway and common.env['wayland'] and common.display_boxes_list:
  279. selection = combo.get_active()
  280. for box in common.display_boxes_list:
  281. box.mode_combo.set_active(selection)
  282. def on_color_chosen(self, user_data, button):
  283. self.color = rgba_to_hex(button.get_rgba())
  284. # clear selected image to indicate it won't be used
  285. self.img.set_from_file("images/empty.png")
  286. common.apply_button.set_sensitive(True)
  287. def on_flip_button(self, button):
  288. # convert images and get (thumbnail path, flipped image path)
  289. images = flip_selected_wallpaper()
  290. self.img.set_from_file(images[0])
  291. self.wallpaper_path = images[1]
  292. self.thumbnail_path = images[0]
  293. self.flip_button.set_sensitive(False)
  294. class SortingButton(Gtk.Button):
  295. def __init__(self):
  296. super().__init__()
  297. self.set_always_show_image(True)
  298. self.img = Gtk.Image()
  299. self.refresh()
  300. self.set_tooltip_text(common.lang['sorting_order'])
  301. self.connect('clicked', self.on_sorting_button)
  302. def refresh(self):
  303. if common.settings.sorting == 'old':
  304. self.img.set_from_file('images/icon_old.svg')
  305. elif common.settings.sorting == 'az':
  306. self.img.set_from_file('images/icon_az.svg')
  307. elif common.settings.sorting == 'za':
  308. self.img.set_from_file('images/icon_za.svg')
  309. else:
  310. self.img.set_from_file('images/icon_new.svg')
  311. self.set_image(self.img)
  312. def on_sorting_button(self, widget):
  313. menu = Gtk.Menu()
  314. i0 = Gtk.MenuItem.new_with_label(common.lang['sorting_new'])
  315. i0.connect('activate', self.on_i0)
  316. menu.append(i0)
  317. i1 = Gtk.MenuItem.new_with_label(common.lang['sorting_old'])
  318. i1.connect('activate', self.on_i1)
  319. menu.append(i1)
  320. i2 = Gtk.MenuItem.new_with_label(common.lang['sorting_az'])
  321. i2.connect('activate', self.on_i2)
  322. menu.append(i2)
  323. i3 = Gtk.MenuItem.new_with_label(common.lang['sorting_za'])
  324. i3.connect('activate', self.on_i3)
  325. menu.append(i3)
  326. menu.show_all()
  327. menu.popup_at_widget(widget, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH_WEST, None)
  328. def on_i0(self, widget):
  329. common.settings.sorting = 'new'
  330. common.settings.save()
  331. self.refresh()
  332. common.preview.refresh()
  333. def on_i1(self, widget):
  334. common.settings.sorting = 'old'
  335. common.settings.save()
  336. self.refresh()
  337. common.preview.refresh()
  338. def on_i2(self, widget):
  339. common.settings.sorting = 'az'
  340. common.settings.save()
  341. self.refresh()
  342. common.preview.refresh()
  343. def on_i3(self, widget):
  344. common.settings.sorting = 'za'
  345. common.settings.save()
  346. self.refresh()
  347. common.preview.refresh()
  348. def on_apply_button(button):
  349. """
  350. Create the command for swaybg (Sway) or feh (X11)
  351. """
  352. # Copy modified wallpapers (if any) from temporary to backgrounds folder
  353. copy_backgrounds()
  354. if common.sway or common.env['wayland']:
  355. # On next startup we'll want to restore what we're setting here. Let's save it to json files
  356. # instead of parsing .azoteb / .fehbg. We only save pictures.
  357. restore_from_file = os.path.join(common.data_home, "swaybg.json")
  358. restore_from = []
  359. # Prepare, save and execute the shell script for swaybg. It'll be placed in ~/.azotebg for further use.
  360. batch_content = ['#!/usr/bin/env bash', 'pkill swaybg']
  361. for box in common.display_boxes_list:
  362. if box.color:
  363. # if a color chosen, the wallpaper won't appear
  364. batch_content.append("swaybg -o {} -c{} &".format(box.display_name, box.color))
  365. elif box.wallpaper_path:
  366. # see: https://github.com/nwg-piotr/azote/issues/143
  367. if common.settings.generic_display_names:
  368. display_name = ""
  369. for item in common.displays:
  370. if item["name"] == box.display_name:
  371. display_name = item["generic-name"]
  372. else:
  373. display_name = box.display_name
  374. # Escape some special characters which would mess up the script
  375. wallpaper_path = box.wallpaper_path.replace('\\', '\\\\').replace("$", "\$").replace("`",
  376. "\\`").replace('"',
  377. '\\"')
  378. batch_content.append(
  379. "swaybg -o '{}' -i \"{}\" -m {} &".format(display_name, wallpaper_path, box.mode))
  380. # build the json file content
  381. if box.wallpaper_path.startswith("{}/backgrounds-sway/flipped-".format(common.data_home)):
  382. thumb = "thumbnail-{}".format(box.wallpaper_path.split("flipped-")[1])
  383. thumb = os.path.join(common.data_home, "backgrounds-sway", thumb)
  384. elif box.wallpaper_path.startswith("{}/backgrounds-sway/part".format(common.data_home)):
  385. thumb = "thumb-part{}".format(box.wallpaper_path.split("part")[1])
  386. thumb = os.path.join(common.data_home, "backgrounds-sway", thumb)
  387. else:
  388. thumb = "{}.png".format(hash_name(box.wallpaper_path))
  389. thumb = os.path.join(common.data_home, "thumbnails", thumb)
  390. entry = {"name": box.display_name, "path": box.wallpaper_path, "thumb": thumb}
  391. restore_from.append(entry)
  392. with open(common.cmd_file, 'w') as f:
  393. for item in batch_content:
  394. f.write("%s\n" % item)
  395. # make the file executable
  396. st = os.stat(common.cmd_file)
  397. os.chmod(common.cmd_file, st.st_mode | stat.S_IEXEC)
  398. subprocess.call(common.cmd_file, shell=True)
  399. save_json(restore_from, restore_from_file)
  400. else:
  401. # As above
  402. restore_from_file = os.path.join(common.data_home, "feh.json")
  403. restore_from = []
  404. # Prepare and execute the feh command. It's being saved automagically to ~/.fehbg
  405. mode = common.display_boxes_list[0].mode # They are all the same, just check the 1st one
  406. command = "feh --bg-{}".format(mode)
  407. c = common.display_boxes_list.copy()
  408. c = sorted(c, key=lambda x: x.xrandr_idx)
  409. for box in c:
  410. command += " '{}'".format(box.wallpaper_path)
  411. # build the json file content
  412. if box.wallpaper_path.startswith("{}/backgrounds-feh/flipped-".format(common.data_home)):
  413. thumb = "thumbnail-{}".format(box.wallpaper_path.split("flipped-")[1])
  414. thumb = os.path.join(common.data_home, "backgrounds-feh", thumb)
  415. elif box.wallpaper_path.startswith("{}/backgrounds-feh/part".format(common.data_home)):
  416. thumb = "thumb-part{}".format(box.wallpaper_path.split("part")[1])
  417. thumb = os.path.join(common.data_home, "backgrounds-feh", thumb)
  418. else:
  419. thumb = "{}.png".format(hash_name(box.wallpaper_path))
  420. thumb = os.path.join(common.data_home, "thumbnails", thumb)
  421. entry = {"name": box.display_name, "path": box.wallpaper_path,
  422. "thumb": thumb}
  423. restore_from.append(entry)
  424. subprocess.call(command, shell=True)
  425. save_json(restore_from, restore_from_file)
  426. def on_split_button(button):
  427. if common.selected_wallpaper:
  428. common.apply_button.set_sensitive(True)
  429. num_parts = 0
  430. for item in common.display_boxes_list:
  431. if item.include:
  432. num_parts += 1
  433. paths = split_selected_wallpaper(num_parts)
  434. i = 0
  435. for box in common.display_boxes_list:
  436. if box.include:
  437. box.wallpaper_path = paths[i][0]
  438. box.img.set_from_file(paths[i][1])
  439. box.thumbnail_path = paths[i][1]
  440. i += 1
  441. if common.display_boxes_list:
  442. for box in common.display_boxes_list:
  443. box.clear_color_selection()
  444. def open_with(item, opener):
  445. # if feh selected as the opener, let's start it with options as below
  446. if opener == 'feh':
  447. command = 'feh --start-at "{}" --scale-down --no-fehbg -d --output-dir {}'.format(
  448. common.selected_wallpaper.source_path, common.selected_wallpaper.folder)
  449. # elif could specify options for other certain programs here
  450. elif opener == 'swappy':
  451. command = 'swappy -f {}'.format(common.selected_wallpaper.source_path)
  452. else:
  453. command = '{} "{}"'.format(opener, common.selected_wallpaper.source_path)
  454. subprocess.Popen(command, shell=True)
  455. def clear_wallpaper_selection():
  456. common.selected_wallpaper = None
  457. common.selected_picture_label.set_text(common.lang['no_picture_selected'])
  458. if common.split_button:
  459. common.split_button.set_sensitive(False)
  460. common.apply_button.set_sensitive(False)
  461. common.apply_to_all_button.set_sensitive(False)
  462. def on_about_button(button):
  463. dialog = Gtk.AboutDialog()
  464. dialog.set_program_name('Azote')
  465. try:
  466. # version = pkg_resources.require(common.app_name)[0].version
  467. dialog.set_version("v{}".format(__version__))
  468. except Exception as e:
  469. print("Couldn't check version: {}".format(e))
  470. pass
  471. logo = GdkPixbuf.Pixbuf.new_from_file_at_size('images/azote.svg', 96, 96)
  472. dialog.set_keep_above(True)
  473. dialog.set_logo(logo)
  474. dialog.set_copyright('(c) 2019-2023 Piotr Miller & Contributors')
  475. dialog.set_website('https://github.com/nwg-piotr/azote')
  476. dialog.set_comments(common.lang['app_desc'])
  477. dialog.set_license_type(Gtk.License.GPL_3_0)
  478. dialog.set_authors(['Piotr Miller (nwg) and Contributors', 'Libraries and dependencies:',
  479. '- colorthief python module (c) 2015 Shipeng Feng',
  480. '- python-pillow (c) 1995-2011, Fredrik Lundh, 2010-2019 Alex Clark and Contributors',
  481. '- pygobject (c) 2005-2019 The GNOME Project',
  482. '- GTK+ (c) 2007-2019 The GTK Team',
  483. '- feh (c) 1999,2000 Tom Gilbert, 2010-2018 Daniel Friesel',
  484. '- swaybg (c) 2016-2019 Drew DeVault',
  485. '- wlr-randr (c) 2019 Purism SPC and Contributors',
  486. '- send2trash python module (c) 2017 Virgil Dupras',
  487. '- grim, slurp (c) 2018 emersion',
  488. '- maim, slop (c) 2014 Dalton Nell and Contributors',
  489. '- imagemagick (c) 1999-2019 ImageMagick Studio LLC',
  490. '- PyYAML (c) 2017-2019 Ingy döt Net Copyright (c) 2006-2016 Kirill Simonov'])
  491. dialog.set_translator_credits('xsme, Leon-Plickat (de_DE), HumanG33k (fr_FR)')
  492. dialog.set_artists(['edskeye'])
  493. dialog.show()
  494. dialog.run()
  495. dialog.destroy()
  496. return False
  497. def move_to_trash(widget):
  498. send2trash(common.selected_wallpaper.source_path)
  499. if os.path.isfile(common.selected_wallpaper.thumb_file):
  500. send2trash(common.selected_wallpaper.thumb_file)
  501. clear_wallpaper_selection()
  502. common.preview.refresh()
  503. def show_image_menu(widget, event=None, parent=None, from_toolbar=False):
  504. cd = current_display()
  505. if common.selected_wallpaper:
  506. if common.associations: # not None if /usr/share/applications/mimeinfo.cache found and parse
  507. openers = common.associations[common.selected_wallpaper.source_path.split('.')[-1].lower()]
  508. menu = Gtk.Menu()
  509. if openers:
  510. for opener in openers:
  511. # opener = (Name, Exec)
  512. item = Gtk.MenuItem.new_with_label(common.lang['open_with'].format(opener[0]))
  513. item.connect('activate', open_with, opener[1])
  514. menu.append(item)
  515. item = Gtk.SeparatorMenuItem()
  516. menu.append(item)
  517. item = Gtk.MenuItem.new_with_label(common.lang['create_palette'])
  518. menu.append(item)
  519. submenu = Gtk.Menu()
  520. # Hell knows why the library does not return the tuple of expected length for some num_colors values
  521. # Let's cheat a little bit
  522. subitem = Gtk.MenuItem.new_with_label('6 {}'.format(common.lang['colors']))
  523. subitem.connect('activate', generate_palette, common.selected_wallpaper.thumb_file,
  524. common.selected_wallpaper.filename,
  525. common.selected_wallpaper.source_path, 6)
  526. submenu.append(subitem)
  527. subitem = Gtk.MenuItem.new_with_label('12 {}'.format(common.lang['colors']))
  528. subitem.connect('activate', generate_palette, common.selected_wallpaper.thumb_file,
  529. common.selected_wallpaper.filename,
  530. common.selected_wallpaper.source_path, 13)
  531. submenu.append(subitem)
  532. subitem = Gtk.MenuItem.new_with_label('18 {}'.format(common.lang['colors']))
  533. subitem.connect('activate', generate_palette, common.selected_wallpaper.thumb_file,
  534. common.selected_wallpaper.filename,
  535. common.selected_wallpaper.source_path, 19)
  536. submenu.append(subitem)
  537. subitem = Gtk.MenuItem.new_with_label('24 {}'.format(common.lang['colors']))
  538. subitem.connect('activate', generate_palette, common.selected_wallpaper.thumb_file,
  539. common.selected_wallpaper.filename,
  540. common.selected_wallpaper.source_path, 25)
  541. submenu.append(subitem)
  542. item.set_submenu(submenu)
  543. item = Gtk.MenuItem.new_with_label(common.lang['scale_and_crop'])
  544. menu.append(item)
  545. submenu = Gtk.Menu()
  546. # Scale and crop to detected displays dimension
  547. for i in range(len(common.displays)):
  548. display = common.displays[i]
  549. width, height = display['width'], display['height']
  550. subitem = Gtk.MenuItem.new_with_label(
  551. '{} x {} ({})'.format(width, height, display['name']))
  552. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path, width, height)
  553. submenu.append(subitem)
  554. # Scale and crop to double width of the primary display
  555. display = common.displays[cd]
  556. width, height = display['width'] * 2, display['height']
  557. subitem = Gtk.MenuItem.new_with_label(
  558. '{} x {} ({} {} {})'.format(width, height, common.lang['current'], display['name'],
  559. common.lang['dual_width']))
  560. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path, width, height)
  561. submenu.append(subitem)
  562. # Scale and crop to double height of the primary display
  563. display = common.displays[cd]
  564. width, height = display['width'], display['height'] * 2
  565. subitem = Gtk.MenuItem.new_with_label(
  566. '{} x {} ({} {} {})'.format(width, height, common.lang['current'], display['name'],
  567. common.lang['dual_height']))
  568. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path, width, height)
  569. submenu.append(subitem)
  570. # Scale and crop to triple width of the primary display
  571. display = common.displays[cd]
  572. width, height = display['width'] * 3, display['height']
  573. subitem = Gtk.MenuItem.new_with_label(
  574. '{} x {} ({} {} {})'.format(width, height, common.lang['current'], display['name'],
  575. common.lang['triple_width']))
  576. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path, width, height)
  577. submenu.append(subitem)
  578. # Scale and crop to triple height of the primary display
  579. display = common.displays[cd]
  580. width, height = display['width'], display['height'] * 3
  581. subitem = Gtk.MenuItem.new_with_label(
  582. '{} x {} ({} {} {})'.format(width, height, common.lang['current'], display['name'],
  583. common.lang['triple_height']))
  584. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path, width, height)
  585. submenu.append(subitem)
  586. # Scale and crop to user-defined dimensions
  587. if common.settings.custom_display:
  588. subitem = Gtk.MenuItem.new_with_label(
  589. '{} x {} ({})'.format(common.settings.custom_display[1], common.settings.custom_display[2],
  590. common.settings.custom_display[0]))
  591. subitem.connect('activate', scale_and_crop, common.selected_wallpaper.source_path,
  592. int(common.settings.custom_display[1]), int(common.settings.custom_display[2]))
  593. submenu.append(subitem)
  594. item.set_submenu(submenu)
  595. if common.env['send2trash']:
  596. item = Gtk.SeparatorMenuItem()
  597. menu.append(item)
  598. item = Gtk.MenuItem.new_with_label(common.lang['remove_image'])
  599. menu.append(item)
  600. submenu = Gtk.Menu()
  601. item1 = Gtk.MenuItem.new_with_label(common.lang['move'])
  602. item1.connect('activate', move_to_trash)
  603. submenu.append(item1)
  604. item.set_submenu(submenu)
  605. menu.show_all()
  606. # We don't want the menu to stick out of the window on Sway, as it may be partially not clickable
  607. menu.popup_at_pointer(None)
  608. else: # fallback in case mimeinfo.cache not found
  609. print("No registered program found. Does the /usr/share/applications/mimeinfo.cache file exist?")
  610. command = 'feh --start-at {} --scale-down --no-fehbg -d --output-dir {}'.format(
  611. common.selected_wallpaper.source_path, common.selected_wallpaper.folder)
  612. subprocess.Popen(command, shell=True)
  613. def on_refresh_clicked(button):
  614. clear_wallpaper_selection()
  615. common.preview.refresh()
  616. def generate_palette(item, thumb_file, filename, image_path, num_colors):
  617. color_thief = ColorThief(image_path)
  618. # dominant = color_thief.get_color(quality=10)
  619. palette = color_thief.get_palette(color_count=num_colors, quality=common.settings.palette_quality)
  620. if common.cpd:
  621. common.cpd.close()
  622. common.cpd = ColorPaletteDialog(thumb_file, filename, palette)
  623. def on_folder_clicked(button):
  624. dialog = Gtk.FileChooserDialog(title=common.lang['open_folder'], parent=button.get_toplevel(),
  625. action=Gtk.FileChooserAction.SELECT_FOLDER)
  626. dialog.set_current_folder(common.settings.src_path)
  627. dialog.add_button(Gtk.STOCK_CANCEL, 0)
  628. dialog.add_button(Gtk.STOCK_OK, 1)
  629. dialog.set_default_response(1)
  630. dialog.set_default_size(800, 600)
  631. response = dialog.run()
  632. if response == 1:
  633. common.settings.src_path = dialog.get_filename()
  634. common.settings.save()
  635. dialog.destroy()
  636. common.preview.refresh()
  637. text = common.settings.src_path
  638. if len(text) > 40:
  639. text = '…{}'.format(text[-38::])
  640. button.set_label(text)
  641. dialog.destroy()
  642. clear_wallpaper_selection()
  643. def destroy(self):
  644. Gtk.main_quit()
  645. def check_height_and_start(window):
  646. w, h = window.get_size()
  647. window.destroy()
  648. if common.sway or common.env['wm'] == "i3":
  649. h = int(h * 0.95)
  650. print(
  651. "Available screen height: {} px; measurement delay: {} ms".format(h, common.settings.screen_measurement_delay))
  652. app = GUI(h)
  653. class TransparentWindow(Gtk.Window):
  654. def __init__(self):
  655. Gtk.Window.__init__(self)
  656. self.connect('draw', self.draw)
  657. self.set_title("Checking...")
  658. # Credits for transparency go to KurtJacobson:
  659. # https://gist.github.com/KurtJacobson/374c8cb83aee4851d39981b9c7e2c22c
  660. screen = self.get_screen()
  661. visual = screen.get_rgba_visual()
  662. if visual and screen.is_composited():
  663. self.set_visual(visual)
  664. self.set_app_paintable(True)
  665. def draw(self, widget, context):
  666. context.set_source_rgba(0, 0, 0, 0.0)
  667. context.set_operator(cairo.OPERATOR_SOURCE)
  668. context.paint()
  669. context.set_operator(cairo.OPERATOR_OVER)
  670. class GUI:
  671. def __init__(self, height):
  672. # set app_id for Wayland
  673. GLib.set_prgname('azote')
  674. window = Gtk.Window()
  675. h = height
  676. window.set_default_size(common.settings.thumb_width * (common.settings.columns + 1), h)
  677. common.main_window = window
  678. window.set_title("Azote~")
  679. window.set_icon_name("azote")
  680. window.set_role("azote")
  681. window.connect_after('destroy', destroy)
  682. window.connect("key-release-event", self.handle_keyboard)
  683. main_box = Gtk.Box()
  684. main_box.set_spacing(5)
  685. main_box.set_border_width(10)
  686. main_box.set_orientation(Gtk.Orientation.VERTICAL)
  687. common.progress_bar = Gtk.ProgressBar()
  688. common.progress_bar.set_fraction(0.0)
  689. common.progress_bar.set_text('0')
  690. common.progress_bar.set_show_text(True)
  691. main_box.pack_start(common.progress_bar, True, False, 0)
  692. window.add(main_box)
  693. window.show_all()
  694. # This contains a Gtk.ScrolledWindow with Gtk.Grid() inside, filled with ThumbButton(Gtk.Button) instances
  695. common.preview = Preview()
  696. main_box.pack_start(common.preview, False, False, 0)
  697. # We need a horizontal container to display outputs in columns
  698. displays_box = Gtk.Box()
  699. displays_box.set_spacing(15)
  700. displays_box.set_orientation(Gtk.Orientation.HORIZONTAL)
  701. # Restore saved wallpapers if any
  702. f_name = "swaybg.json" if common.sway or common.env['wayland'] else "feh.json"
  703. f_path = os.path.join(common.data_home, f_name)
  704. if os.path.isfile(f_path):
  705. restore_from = load_json(f_path)
  706. else:
  707. restore_from = None
  708. # Buttons below represent displays preview
  709. common.display_boxes_list = []
  710. for display in common.displays:
  711. name = display.get('name')
  712. # Check if we have stored values
  713. path, thumb = None, None
  714. if restore_from:
  715. for item in restore_from:
  716. if item["name"] == name:
  717. path = item["path"]
  718. thumb = item["thumb"]
  719. # Label format: name (width x height)
  720. try:
  721. xrandr_idx = display.get('xrandr-idx')
  722. except KeyError:
  723. xrandr_idx = None
  724. display_box = DisplayBox(name, display.get('width'), display.get('height'), path, thumb, xrandr_idx)
  725. common.display_boxes_list.append(display_box)
  726. displays_box.pack_start(display_box, True, False, 0)
  727. main_box.pack_start(displays_box, False, False, 0)
  728. # Bottom buttons will also need a horizontal container
  729. bottom_box = Gtk.Box()
  730. bottom_box.set_spacing(5)
  731. bottom_box.set_border_width(5)
  732. bottom_box.set_orientation(Gtk.Orientation.HORIZONTAL)
  733. # Button to change sorting order
  734. sorting_button = SortingButton()
  735. bottom_box.add(sorting_button)
  736. # Button to refresh currently selected folder thumbnails
  737. refresh_button = Gtk.Button()
  738. refresh_button.set_always_show_image(True)
  739. img = Gtk.Image()
  740. img.set_from_file('images/icon_refresh.svg')
  741. refresh_button.set_image(img)
  742. refresh_button.set_tooltip_text(common.lang['refresh_folder_preview'])
  743. bottom_box.add(refresh_button)
  744. refresh_button.connect_after('clicked', on_refresh_clicked)
  745. # Button to set the wallpapers folder
  746. folder_button = Gtk.Button.new_with_label(common.settings.src_path)
  747. folder_button.set_property("name", "folder-btn")
  748. folder_button.set_tooltip_text(common.lang['open_another_folder'])
  749. bottom_box.pack_start(folder_button, True, True, 0)
  750. folder_button.connect_after('clicked', on_folder_clicked)
  751. # Label to display details of currently selected picture
  752. common.selected_picture_label = Gtk.Label()
  753. common.selected_picture_label.set_property("name", "selected-label")
  754. common.selected_picture_label.set_text(common.lang['no_picture_selected'])
  755. bottom_box.pack_start(common.selected_picture_label, True, True, 0)
  756. # Button to split wallpaper between displays
  757. if len(common.displays) > 1:
  758. common.split_button = Gtk.Button()
  759. common.split_button.set_always_show_image(True)
  760. img = Gtk.Image()
  761. img.set_from_file('images/icon_split.svg')
  762. common.split_button.set_image(img)
  763. bottom_box.add(common.split_button)
  764. common.split_button.set_sensitive(False)
  765. common.split_button.set_tooltip_text(common.lang['split_selection_between_displays'])
  766. common.split_button.connect('clicked', on_split_button)
  767. # Button to apply selected wallpaper to all displays (connected at the moment or not)
  768. common.apply_to_all_button = Gtk.Button()
  769. common.apply_to_all_button.set_always_show_image(True)
  770. img = Gtk.Image()
  771. img.set_from_file('images/icon_all.svg')
  772. common.apply_to_all_button.set_image(img)
  773. common.apply_to_all_button.connect('clicked', on_apply_to_all_button)
  774. common.apply_to_all_button.set_sensitive(False)
  775. common.apply_to_all_button.set_tooltip_text(common.lang['apply_to_all'])
  776. bottom_box.add(common.apply_to_all_button)
  777. # Button to apply settings
  778. names = ''
  779. for display in common.displays:
  780. names += '{} '.format(display['name'])
  781. common.apply_button = Gtk.Button()
  782. common.apply_button.set_always_show_image(True)
  783. img = Gtk.Image()
  784. img.set_from_file('images/icon_apply.svg')
  785. common.apply_button.set_image(img)
  786. common.apply_button.connect('clicked', on_apply_button)
  787. common.apply_button.set_sensitive(False)
  788. common.apply_button.set_tooltip_text(common.lang['apply_settings'].format(names))
  789. bottom_box.add(common.apply_button)
  790. main_box.add(bottom_box)
  791. h_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
  792. main_box.add(h_separator)
  793. # Another horizontal container for the status line + button(s)
  794. status_box = Gtk.Box()
  795. status_box.set_spacing(5)
  796. status_box.set_border_width(5)
  797. status_box.set_orientation(Gtk.Orientation.HORIZONTAL)
  798. # Button to call About dialog
  799. about_button = Gtk.Button()
  800. about_button.set_always_show_image(True)
  801. img = Gtk.Image()
  802. img.set_from_file('images/icon_about.svg')
  803. about_button.set_image(img)
  804. about_button.set_tooltip_text(common.lang['about_azote'])
  805. about_button.connect('clicked', on_about_button)
  806. status_box.add(about_button)
  807. # Button to display settings menu
  808. settings_button = Gtk.Button()
  809. settings_button.set_always_show_image(True)
  810. img = Gtk.Image()
  811. img.set_from_file('images/icon_menu.svg')
  812. settings_button.set_image(img)
  813. settings_button.set_tooltip_text(common.lang['preferences'])
  814. settings_button.connect('clicked', on_settings_button)
  815. status_box.add(settings_button)
  816. # Color picker button
  817. picker_button = Gtk.Button()
  818. picker_button.set_always_show_image(True)
  819. img = Gtk.Image()
  820. img.set_from_file('images/icon_picker.svg')
  821. picker_button.set_image(img)
  822. picker_button.set_sensitive(common.picker)
  823. if common.sway or common.env['wayland']:
  824. tt = common.lang['screen_color_picker'] if common.picker else common.lang['grim_slurp_required']
  825. else:
  826. tt = common.lang['screen_color_picker'] if common.picker else common.lang['maim_slop_required']
  827. picker_button.set_tooltip_text(tt)
  828. picker_button.connect('clicked', on_picker_button)
  829. status_box.add(picker_button)
  830. # dotfiles button
  831. dotfiles_button = Gtk.Button()
  832. dotfiles_button.set_always_show_image(True)
  833. img = Gtk.Image()
  834. img.set_from_file('images/icon_config.svg')
  835. dotfiles_button.set_image(img)
  836. active = common.xresources or common.alacritty_config and common.env['yaml']
  837. if active:
  838. dotfiles_button.set_tooltip_text(common.lang['dotfiles'])
  839. else:
  840. dotfiles_button.set_tooltip_text(common.lang['check_log'].format(common.log_file))
  841. dotfiles_button.set_sensitive(False)
  842. dotfiles_button.connect('clicked', on_dotfiles_button)
  843. status_box.add(dotfiles_button)
  844. common.status_bar = Gtk.Statusbar()
  845. common.status_bar.set_property("name", "status-bar")
  846. common.status_bar.set_halign(Gtk.Align.CENTER)
  847. status_box.pack_start(common.status_bar, True, True, 0)
  848. update_status_bar()
  849. btn = Gtk.Button.new_with_label(common.lang["close"])
  850. btn.connect("clicked", Gtk.main_quit)
  851. btn.set_property("valign", Gtk.Align.CENTER)
  852. status_box.pack_end(btn, False, False, 0)
  853. main_box.add(status_box)
  854. window.show_all()
  855. deselect_all()
  856. common.progress_bar.hide()
  857. def handle_keyboard(self, item, event):
  858. if event.type == Gdk.EventType.KEY_RELEASE and event.keyval == Gdk.KEY_Escape:
  859. Gtk.main_quit()
  860. return True
  861. def on_apply_to_all_button(button):
  862. """
  863. This will create a single command to set the same wallpaper to all displays, CONNECTED at the time OR NOT.
  864. Menu for modes needs to differ for swaybg and feh.
  865. """
  866. menu = Gtk.Menu()
  867. if common.sway or common.env['wayland']:
  868. for mode in common.modes_swaybg:
  869. item = Gtk.MenuItem.new_with_label(mode)
  870. item.connect('activate', apply_to_all_swaybg, mode)
  871. menu.append(item)
  872. menu.show_all()
  873. menu.popup_at_widget(button, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH_EAST, None)
  874. else:
  875. for mode in common.modes_feh:
  876. item = Gtk.MenuItem.new_with_label(mode)
  877. item.connect('activate', apply_to_all_feh, mode)
  878. menu.append(item)
  879. menu.show_all()
  880. menu.popup_at_widget(button, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH_EAST, None)
  881. def on_settings_button(button):
  882. menu = Gtk.Menu()
  883. item = Gtk.MenuItem.new_with_label(common.lang['custom_display'])
  884. item.connect('activate', show_custom_display_dialog)
  885. menu.append(item)
  886. item = Gtk.CheckMenuItem.new_with_label(common.lang['color_dictionary'])
  887. item.set_active(common.settings.color_dictionary)
  888. item.connect('activate', switch_color_dictionary)
  889. menu.append(item)
  890. item = Gtk.CheckMenuItem.new_with_label(common.lang['image_menu_button'])
  891. item.set_active(common.settings.image_menu_button)
  892. item.connect('activate', switch_image_menu_button)
  893. menu.append(item)
  894. item = Gtk.CheckMenuItem.new_with_label(common.lang['track_file_changes'])
  895. item.set_active(common.settings.track_files)
  896. item.connect('activate', switch_tracking_files)
  897. menu.append(item)
  898. item = Gtk.CheckMenuItem.new_with_label(common.lang['use_display_names'])
  899. item.set_active(common.settings.generic_display_names)
  900. item.connect('activate', switch_generic_display_names)
  901. menu.append(item)
  902. menu.show_all()
  903. menu.popup_at_widget(button, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH_WEST, None)
  904. def pick_color():
  905. """
  906. In sway we'll just use grim & slurp to pick a color: it returns accurate values.
  907. On X11 same should be possible with maim & slop, but it happens to crash.
  908. In both cases we'll use the same fallback: calculate the dominant colour of a selected region with colorthief.
  909. This is less accurate, alas.
  910. :return: tuple (rrr, ggg, bbb)
  911. """
  912. color = (255, 255, 255)
  913. if common.sway or common.env['wayland']:
  914. try:
  915. color = hex_to_rgb(subprocess.check_output(
  916. 'grim -g "$(slurp -p)" -t ppm - | convert - -format \'%[pixel:p{0,0}]\' txt:- | awk \'NR==2 {print $3}\'',
  917. shell=True).decode("utf-8"))
  918. except:
  919. try:
  920. color = get_dominant_from_area()
  921. except:
  922. pass
  923. else:
  924. try:
  925. output = subprocess.check_output('maim -st 0 | convert - -resize 1x1! -format \'%[pixel:p{0,0}]\' info:-',
  926. shell=True).decode("utf-8")
  927. values = output[6:-1].split(",")
  928. color = (int(values[0]), int(values[1]), int(values[2]))
  929. except:
  930. try:
  931. color = get_dominant_from_area()
  932. except:
  933. pass
  934. return color
  935. def get_dominant_from_area():
  936. """
  937. Saves selected area with `grim -g "$(slurp)" common.tmp_dir/azote/temp/area.png`,
  938. then calculates the dominant color with the colorthief module.
  939. :return: tuple (r, g, b) or (255, 255, 255) if nothing selected
  940. """
  941. dominant = (255, 255, 255)
  942. if common.sway or common.env['wayland']:
  943. cmd = 'grim -g "$(slurp)" {}'.format(os.path.join(common.tmp_dir, 'area.png'))
  944. else:
  945. cmd = 'maim -s {}'.format(os.path.join(common.tmp_dir, 'area.png'))
  946. res = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL).returncode
  947. if res == 0:
  948. color_thief = ColorThief(os.path.join(common.tmp_dir, 'area.png'))
  949. try:
  950. dominant = color_thief.get_color(quality=common.settings.palette_quality)
  951. except:
  952. pass
  953. return dominant
  954. def on_picker_button(button):
  955. if common.picker_window:
  956. common.picker_window.close()
  957. color = pick_color()
  958. common.picker_window = ColorPickerDialog(color)
  959. def on_dotfiles_button(button):
  960. if common.xresources or common.alacritty_config:
  961. menu = Gtk.Menu()
  962. if common.xresources:
  963. item = Gtk.MenuItem.new_with_label(common.xresources)
  964. item.connect('activate', open_dotfile, 'xresources')
  965. menu.append(item)
  966. if common.env['yaml'] and common.alacritty_config:
  967. item = Gtk.MenuItem.new_with_label(common.alacritty_config)
  968. item.connect('activate', open_dotfile, 'alacritty')
  969. menu.append(item)
  970. menu.show_all()
  971. menu.popup_at_widget(button, Gdk.Gravity.CENTER, Gdk.Gravity.NORTH_WEST, None)
  972. def open_dotfile(widget, which):
  973. if common.dotfile_window:
  974. common.dotfile_window.close()
  975. if which == 'alacritty' and common.alacritty_config:
  976. common.dotfile_window = Alacritty()
  977. elif which == 'xresources' and common.xresources:
  978. common.dotfile_window = Xresources()
  979. def show_custom_display_dialog(item):
  980. cdd = CustomDisplayDialog()
  981. def switch_color_dictionary(item):
  982. if item.get_active():
  983. common.settings.color_dictionary = True
  984. common.settings.save()
  985. else:
  986. common.settings.color_dictionary = False
  987. common.settings.save()
  988. def switch_image_menu_button(item):
  989. if item.get_active():
  990. common.settings.image_menu_button = True
  991. common.settings.save()
  992. else:
  993. common.settings.image_menu_button = False
  994. common.settings.save()
  995. def switch_tracking_files(item):
  996. if item.get_active():
  997. common.settings.track_files = True
  998. common.settings.save()
  999. GLib.timeout_add_seconds(common.settings.tracking_interval_seconds, track_changes)
  1000. else:
  1001. common.settings.track_files = False
  1002. common.settings.save()
  1003. if common.indicator:
  1004. common.indicator.switch_indication(item)
  1005. def switch_generic_display_names(item):
  1006. if item.get_active():
  1007. common.settings.generic_display_names = True
  1008. common.settings.save()
  1009. else:
  1010. common.settings.generic_display_names = False
  1011. common.settings.save()
  1012. class ColorPaletteDialog(Gtk.Window):
  1013. def __init__(self, thumb_file, filename, palette):
  1014. super().__init__()
  1015. self.image = Gtk.Image.new_from_file(thumb_file)
  1016. self.label = Gtk.Label()
  1017. self.label.set_text(filename)
  1018. self.label.set_property('name', 'image-label')
  1019. self.set_title(filename)
  1020. self.set_role("toolbox")
  1021. self.set_resizable(False)
  1022. self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
  1023. # self.set_transient_for(common.main_window)
  1024. self.set_position(Gtk.WindowPosition.MOUSE)
  1025. self.set_keep_above(True)
  1026. self.all_buttons = []
  1027. try:
  1028. self.copy_as = common.settings.copy_as
  1029. except AttributeError:
  1030. self.copy_as = '#rgb'
  1031. self.vbox = Gtk.VBox()
  1032. self.vbox.set_spacing(5)
  1033. self.vbox.set_border_width(15)
  1034. self.vbox.pack_start(self.image, True, True, 15)
  1035. self.vbox.add(self.label)
  1036. self.hbox = Gtk.HBox()
  1037. self.hbox.set_spacing(5)
  1038. self.hbox.set_border_width(5)
  1039. for i in range(len(palette)):
  1040. color = palette[i]
  1041. hex_color = rgb_to_hex(color)
  1042. pixbuf = create_pixbuf((common.settings.color_icon_w, common.settings.color_icon_h), color)
  1043. gtk_image = Gtk.Image.new_from_pixbuf(pixbuf)
  1044. button = Gtk.Button.new_with_label(hex_color)
  1045. button.set_always_show_image(True)
  1046. button.set_image(gtk_image)
  1047. button.set_image_position(2) # TOP
  1048. if common.settings.color_dictionary:
  1049. exact, closest = common.color_names.get_colour_name(hex_color)
  1050. if exact:
  1051. name = common.lang['exact'].format(exact)
  1052. else:
  1053. name = common.lang['closest'].format(closest)
  1054. button.set_tooltip_text('{}'.format(name))
  1055. else:
  1056. button.set_tooltip_text(common.lang['copy'])
  1057. button.connect_after('clicked', self.to_clipboard)
  1058. self.all_buttons.append(button)
  1059. self.hbox.pack_start(button, False, False, 0)
  1060. if (i + 1) % 6 == 0:
  1061. self.vbox.add(self.hbox)
  1062. self.hbox = Gtk.HBox()
  1063. self.hbox.set_spacing(5)
  1064. self.hbox.set_border_width(5)
  1065. button1 = Gtk.RadioButton(label='#rgb')
  1066. button1.connect('toggled', self.rgb_toggled)
  1067. button2 = Gtk.RadioButton.new_with_label_from_widget(button1, 'r, g, b')
  1068. button2.set_label('r, g, b')
  1069. button2.connect('toggled', self.rgb_toggled)
  1070. for button in [button1, button2]:
  1071. if button.get_label() == self.copy_as:
  1072. button.set_active(True)
  1073. label = Gtk.Label()
  1074. label.set_text(common.lang['copy_as'])
  1075. hbox = Gtk.HBox()
  1076. hbox.set_spacing(5)
  1077. hbox.set_border_width(5)
  1078. hbox.pack_start(label, True, False, 0)
  1079. hbox.pack_start(button1, True, False, 0)
  1080. hbox.pack_start(button2, True, False, 0)
  1081. self.vbox.pack_start(hbox, True, True, 0)
  1082. hbox = Gtk.HBox()
  1083. hbox.set_spacing(5)
  1084. hbox.set_border_width(5)
  1085. self.clipboard_preview = ClipboardPreview()
  1086. hbox.pack_start(self.clipboard_preview, False, False, 0)
  1087. self.clipboard_label = Gtk.Label()
  1088. self.clipboard_label.set_text(common.lang['clipboard_empty'])
  1089. hbox.pack_start(self.clipboard_label, True, True, 0)
  1090. button = Gtk.Button.new_with_label(common.lang['close'])
  1091. button.connect_after('clicked', self.close_window)
  1092. hbox.pack_start(button, False, False, 0)
  1093. self.vbox.add(hbox)
  1094. self.add(self.vbox)
  1095. self.show_all()
  1096. def rgb_toggled(self, button):
  1097. state = 'on' if button.get_active() else 'off'
  1098. if state == 'on':
  1099. common.settings.copy_as = button.get_label()
  1100. common.settings.save()
  1101. def close_window(self, widget):
  1102. self.close()
  1103. def show(self):
  1104. self.show_all()
  1105. def to_clipboard(self, widget):
  1106. # clear selection
  1107. for button in self.all_buttons:
  1108. button.set_property("name", "color-btn")
  1109. # mark selected
  1110. widget.set_property("name", "color-btn-selected")
  1111. common.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  1112. if common.settings.copy_as == 'r, g, b':
  1113. t = hex_to_rgb(widget.get_label())
  1114. content = '{}, {}, {}'.format(t[0], t[1], t[2])
  1115. else:
  1116. content = widget.get_label()
  1117. common.clipboard.set_text(content, -1)
  1118. common.clipboard_text = widget.get_label()
  1119. label = '{}'.format(content)
  1120. self.clipboard_preview.update(widget.get_label())
  1121. self.clipboard_label.set_text(label)
  1122. class ClipboardPreview(Gtk.ColorButton):
  1123. def __init__(self):
  1124. super().__init__()
  1125. rgba = rgb_to_rgba((255, 255, 255))
  1126. color = Gdk.RGBA()
  1127. color.red = rgba[0]
  1128. color.green = rgba[1]
  1129. color.blue = rgba[2]
  1130. color.alpha = rgba[3]
  1131. self.set_rgba(color)
  1132. self.connect("color-set", self.to_clipboard)
  1133. def update(self, color):
  1134. rgb = hex_to_rgb(color)
  1135. rgba = rgb_to_rgba(rgb)
  1136. color = Gdk.RGBA()
  1137. color.red = rgba[0]
  1138. color.green = rgba[1]
  1139. color.blue = rgba[2]
  1140. color.alpha = rgba[3]
  1141. self.set_rgba(color)
  1142. def to_clipboard(self, widget):
  1143. common.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  1144. rgba = self.get_rgba()
  1145. if common.settings.copy_as == 'r, g, b':
  1146. hex = rgba_to_hex(rgba)
  1147. t = hex_to_rgb(hex)
  1148. content = '{}, {}, {}'.format(t[0], t[1], t[2])
  1149. else:
  1150. content = rgba_to_hex(rgba)
  1151. hex = content
  1152. common.clipboard.set_text(content, -1)
  1153. common.clipboard_text = hex
  1154. label = '{}'.format(content)
  1155. common.cpd.clipboard_label.set_text(label)
  1156. class ColorPickerDialog(Gtk.Window):
  1157. def __init__(self, color):
  1158. super().__init__()
  1159. if not color:
  1160. color = (255, 255, 255)
  1161. self.set_title(common.lang['screen_color_picker'])
  1162. self.set_role("toolbox")
  1163. self.set_resizable(False)
  1164. self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
  1165. # self.set_transient_for(common.main_window)
  1166. self.set_position(Gtk.WindowPosition.MOUSE)
  1167. self.set_keep_above(True)
  1168. try:
  1169. self.copy_as = common.settings.copy_as
  1170. except AttributeError:
  1171. self.copy_as = '#rgb'
  1172. self.vbox = Gtk.VBox()
  1173. self.vbox.set_spacing(5)
  1174. self.vbox.set_border_width(5)
  1175. rgba = rgb_to_rgba(color)
  1176. self.color_button = Gtk.ColorButton()
  1177. rgba_color = Gdk.RGBA()
  1178. rgba_color.red = rgba[0]
  1179. rgba_color.green = rgba[1]
  1180. rgba_color.blue = rgba[2]
  1181. rgba_color.alpha = 1.0
  1182. self.color_button.set_rgba(rgba_color)
  1183. self.color_button.connect("color-set", self.on_color_chosen, self.color_button)
  1184. self.color_button.set_tooltip_text(common.lang['background_color'])
  1185. self.vbox.pack_start(self.color_button, False, False, 0)
  1186. self.label = Gtk.Label()
  1187. self.label.set_text(rgb_to_hex(color))
  1188. self.vbox.add(self.label)
  1189. if common.settings.color_dictionary:
  1190. self.closest_label = Gtk.Label()
  1191. self.closest_label.set_property('name', 'closest')
  1192. exact, closest = common.color_names.get_colour_name(rgb_to_hex(color))
  1193. if exact:
  1194. name = common.lang['exact'].format(exact)
  1195. else:
  1196. name = common.lang['closest'].format(closest)
  1197. self.closest_label.set_text(name)
  1198. self.vbox.add(self.closest_label)
  1199. button1 = Gtk.RadioButton(label='#rgb')
  1200. button1.connect('toggled', self.rgb_toggled)
  1201. button2 = Gtk.RadioButton.new_with_label_from_widget(button1, 'r, g, b')
  1202. button2.set_label('r, g, b')
  1203. button2.connect('toggled', self.rgb_toggled)
  1204. for button in [button1, button2]:
  1205. if button.get_label() == self.copy_as:
  1206. button.set_active(True)
  1207. hbox = Gtk.HBox()
  1208. hbox.set_spacing(5)
  1209. hbox.set_border_width(5)
  1210. hbox.pack_start(button1, True, False, 0)
  1211. hbox.pack_start(button2, True, False, 0)
  1212. self.vbox.add(hbox)
  1213. hbox = Gtk.HBox()
  1214. hbox.set_spacing(5)
  1215. hbox.set_border_width(5)
  1216. button = Gtk.Button.new_with_label(common.lang['copy'])
  1217. button.connect_after('clicked', self.to_clipboard)
  1218. hbox.pack_start(button, True, False, 0)
  1219. button = Gtk.Button()
  1220. button.set_always_show_image(True)
  1221. img = Gtk.Image()
  1222. img.set_from_file('images/icon_picker.svg')
  1223. button.set_image(img)
  1224. button.connect_after('clicked', self.pick_new_color)
  1225. hbox.pack_start(button, True, False, 0)
  1226. button = Gtk.Button.new_with_label(common.lang['close'])
  1227. button.connect_after('clicked', self.close_window)
  1228. hbox.pack_start(button, True, False, 0)
  1229. self.vbox.add(hbox)
  1230. self.add(self.vbox)
  1231. self.show_all()
  1232. def on_color_chosen(self, user_data, button):
  1233. self.label.set_text(rgba_to_hex(button.get_rgba()))
  1234. exact, closest = common.color_names.get_colour_name(self.label.get_text())
  1235. if exact:
  1236. name = common.lang['exact'].format(exact)
  1237. else:
  1238. name = common.lang['closest'].format(closest)
  1239. self.closest_label.set_text(name)
  1240. def rgb_toggled(self, button):
  1241. state = 'on' if button.get_active() else 'off'
  1242. if state == 'on':
  1243. common.settings.copy_as = button.get_label()
  1244. common.settings.save()
  1245. def close_window(self, widget):
  1246. self.close()
  1247. def to_clipboard(self, widget):
  1248. common.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  1249. if common.settings.copy_as == 'r, g, b':
  1250. t = hex_to_rgb(self.label.get_text())
  1251. content = '{}, {}, {}'.format(t[0], t[1], t[2])
  1252. else:
  1253. content = self.label.get_text()
  1254. common.clipboard.set_text(content, -1)
  1255. common.clipboard_text = self.label.get_text()
  1256. def pick_new_color(self, button):
  1257. color = pick_color()
  1258. if not color:
  1259. color = (255, 255, 255)
  1260. rgba = rgb_to_rgba(color)
  1261. rgba_color = Gdk.RGBA()
  1262. rgba_color.red = rgba[0]
  1263. rgba_color.green = rgba[1]
  1264. rgba_color.blue = rgba[2]
  1265. rgba_color.alpha = 1.0
  1266. self.color_button.set_rgba(rgba_color)
  1267. self.label.set_text(rgb_to_hex(color))
  1268. if common.settings.color_dictionary:
  1269. exact, closest = common.color_names.get_colour_name(rgb_to_hex(color))
  1270. if exact:
  1271. name = common.lang['exact'].format(exact)
  1272. else:
  1273. name = common.lang['closest'].format(closest)
  1274. self.closest_label.set_text(name)
  1275. self.vbox.add(self.closest_label)
  1276. class CustomDisplayDialog(Gtk.Window):
  1277. def __init__(self):
  1278. super().__init__()
  1279. self.properties = common.settings.custom_display
  1280. self.set_title("Azote custom display")
  1281. self.set_role("toolbox")
  1282. self.set_resizable(False)
  1283. self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
  1284. self.set_decorated(False)
  1285. # self.set_transient_for(common.main_window)
  1286. self.set_position(Gtk.WindowPosition.MOUSE)
  1287. self.set_keep_above(True)
  1288. self.name_label = Gtk.Label()
  1289. self.name_label.set_width_chars(12)
  1290. self.name_label.set_text(common.lang['name'])
  1291. self.name_entry = Gtk.Entry()
  1292. if self.properties and self.properties[0]:
  1293. self.name_entry.set_text(self.properties[0])
  1294. self.name_entry.connect('changed', self.validate_entries)
  1295. self.width_label = Gtk.Label()
  1296. self.width_label.set_width_chars(12)
  1297. self.width_label.set_text(common.lang['width'])
  1298. self.button_ok = Gtk.Button.new_with_label(common.lang['ok'])
  1299. self.button_ok.set_sensitive(False)
  1300. self.width_entry = NumberEntry()
  1301. if self.properties:
  1302. self.width_entry.set_text(self.properties[1])
  1303. self.width_entry.connect('changed', self.validate_entries)
  1304. self.height_label = Gtk.Label()
  1305. self.height_label.set_width_chars(12)
  1306. self.height_label.set_text(common.lang['height'])
  1307. self.height_entry = NumberEntry()
  1308. if self.properties:
  1309. self.height_entry.set_text(self.properties[2])
  1310. self.height_entry.connect('changed', self.validate_entries)
  1311. self.button_cancel = Gtk.Button.new_with_label(common.lang['cancel'])
  1312. self.button_cancel.connect("clicked", self.dialog_cancel, self)
  1313. self.button_clear = Gtk.Button.new_with_label(common.lang['delete'])
  1314. self.button_clear.connect("clicked", self.dialog_clear, self)
  1315. self.vbox = Gtk.VBox()
  1316. self.vbox.set_spacing(5)
  1317. self.vbox.set_border_width(5)
  1318. self.hbox0 = Gtk.HBox()
  1319. self.hbox0.pack_start(self.name_label, True, True, 0)
  1320. self.hbox0.add(self.name_entry)
  1321. self.vbox.add(self.hbox0)
  1322. self.hbox1 = Gtk.HBox()
  1323. self.hbox1.pack_start(self.width_label, True, True, 0)
  1324. self.hbox1.add(self.width_entry)
  1325. self.vbox.add(self.hbox1)
  1326. self.hbox2 = Gtk.HBox()
  1327. self.hbox2.pack_start(self.height_label, True, True, 0)
  1328. self.hbox2.add(self.height_entry)
  1329. self.vbox.add(self.hbox2)
  1330. self.hbox3 = Gtk.HBox()
  1331. self.hbox3.pack_start(self.button_ok, True, True, 0)
  1332. self.hbox3.pack_start(self.button_cancel, True, True, 5)
  1333. self.hbox3.pack_start(self.button_clear, True, True, 0)
  1334. self.vbox.pack_start(self.hbox3, True, True, 0)
  1335. self.add(self.vbox)
  1336. self.button_ok.connect("clicked", self.dialog_ok)
  1337. self.show_all()
  1338. def validate_entries(self, widget):
  1339. self.button_ok.set_sensitive(self.width_entry.get_text() and self.height_entry.get_text())
  1340. def dialog_ok(self, widget, callback_data=None):
  1341. self.properties = [self.name_entry.get_text(), self.width_entry.get_text(), self.height_entry.get_text()]
  1342. if not self.properties[0]:
  1343. self.properties[0] = 'Custom'
  1344. common.settings.custom_display = self.properties
  1345. common.settings.save()
  1346. self.close()
  1347. def dialog_cancel(self, widget, callback_data=None):
  1348. self.close()
  1349. def dialog_clear(self, widget, callback_data=None):
  1350. common.settings.custom_display = None
  1351. common.settings.save()
  1352. self.close()
  1353. class NumberEntry(Gtk.Entry):
  1354. """
  1355. https://stackoverflow.com/a/2727085/4040598
  1356. """
  1357. def __init__(self):
  1358. Gtk.Entry.__init__(self)
  1359. self.connect('changed', self.on_changed)
  1360. def on_changed(self, *args):
  1361. text = self.get_text().strip()
  1362. self.set_text(''.join([i for i in text if i in '0123456789']))
  1363. def dialog_cancel(widget, window, callback_data=None):
  1364. window.close()
  1365. def on_thumb_double_click(button):
  1366. """
  1367. As the function above, but mode 'fill' will always be used
  1368. """
  1369. if common.sway or common.env['wayland']:
  1370. apply_to_all_swaybg(button, 'fill')
  1371. else:
  1372. apply_to_all_feh(button, 'fill')
  1373. def apply_to_all_swaybg(item, mode):
  1374. # Firstly we need to set the selected image thumbnail to all previews currently visible
  1375. for box in common.display_boxes_list:
  1376. box.img.set_from_file(common.selected_wallpaper.thumb_file)
  1377. box.wallpaper_path = common.selected_wallpaper.source_path
  1378. box.thumbnail_path = common.selected_wallpaper.thumb_file
  1379. common.apply_button.set_sensitive(True)
  1380. def apply_to_all_feh(item, mode):
  1381. # Firstly we need to set the selected image thumbnail to all previews currently visible
  1382. for box in common.display_boxes_list:
  1383. box.img.set_from_file(common.selected_wallpaper.thumb_file)
  1384. box.wallpaper_path = common.selected_wallpaper.source_path
  1385. box.thumbnail_path = common.selected_wallpaper.thumb_file
  1386. common.apply_button.set_sensitive(True)
  1387. def print_help():
  1388. print('\nAzote wallpaper manager version {}\n'.format(__version__))
  1389. print('[-h] | [--help]\t\t\t print Help')
  1390. print('[-l] | [--lang] <ln_LN> \t force a Locale (de_DE, en_US, fr_FR, pl_PL)')
  1391. print('[-c] | [--clear]\t\t Clear unused thumbnails')
  1392. print('[-a] | [--clear-all]\t\t clear All thumbnails\n')
  1393. print('[-v] | [--version]\t\t display Version information\n')
  1394. def track_changes():
  1395. if common.preview and common.settings.src_path:
  1396. files_dict = dict([(f, None) for f in os.listdir(common.settings.src_path)])
  1397. if not files_dict == common.preview.files_dict:
  1398. common.preview.refresh()
  1399. return common.settings.track_files
  1400. class Indicator(object):
  1401. def __init__(self):
  1402. self.ind = AppIndicator3.Indicator.new('azote_status_icon', '',
  1403. AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
  1404. self.ind.set_icon_full('/usr/share/azote/indicator_active.png', 'Tracking off')
  1405. self.ind.set_attention_icon_full('/usr/share/azote/indicator_attention.png', 'Tracking on')
  1406. if common.settings.track_files:
  1407. self.ind.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
  1408. if common.sway or common.env['wayland']:
  1409. self.ind.set_icon_full('/usr/share/azote/indicator_attention.png', 'Tracking on')
  1410. else:
  1411. self.ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
  1412. if common.sway or common.env['wayland']:
  1413. self.ind.set_icon_full('/usr/share/azote/indicator_active.png', 'Tracking off')
  1414. self.ind.set_menu(self.menu())
  1415. def menu(self):
  1416. menu = Gtk.Menu()
  1417. item = Gtk.MenuItem.new_with_label(common.lang['clear_unused_thumbnails'])
  1418. item.connect('activate', self.clear_unused)
  1419. menu.append(item)
  1420. item = Gtk.MenuItem.new_with_label(common.lang['about_azote'])
  1421. item.connect('activate', on_about_button)
  1422. menu.append(item)
  1423. item = Gtk.SeparatorMenuItem()
  1424. menu.append(item)
  1425. item = Gtk.MenuItem.new_with_label(common.lang['exit'])
  1426. item.connect('activate', destroy)
  1427. menu.append(item)
  1428. menu.show_all()
  1429. return menu
  1430. def clear_unused(self, item):
  1431. clear_thumbnails()
  1432. common.preview.refresh()
  1433. def switch_indication(self, item):
  1434. if item.get_active():
  1435. self.ind.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
  1436. if common.sway or common.env['wayland']:
  1437. self.ind.set_icon_full('/usr/share/azote/indicator_attention.png', 'Tracking on')
  1438. else:
  1439. if common.sway or common.env['wayland']:
  1440. self.ind.set_icon_full('/usr/share/azote/indicator_active.png', 'Tracking off')
  1441. self.ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
  1442. def main():
  1443. lang_from_args = None
  1444. clear_thumbs, clear_all = False, False
  1445. common.color_names = WikiColours()
  1446. for i in range(1, len(sys.argv)):
  1447. if sys.argv[i].upper() == '-H' or sys.argv[i].upper() == '--HELP':
  1448. print_help()
  1449. exit(0)
  1450. if sys.argv[i].upper() == '-L' or sys.argv[i].upper() == '--LANG':
  1451. try:
  1452. lang_from_args = sys.argv[i + 1]
  1453. except:
  1454. pass
  1455. if sys.argv[i].upper() == '-C' or sys.argv[i].upper() == '--CLEAR':
  1456. clear_thumbs = True
  1457. if sys.argv[i].upper() == '-A' or sys.argv[i].upper() == '--CLEAR-ALL':
  1458. clear_thumbs, clear_all = True, True
  1459. if sys.argv[i].upper() == '-V' or sys.argv[i].upper() == '--VERSION':
  1460. print("Azote version {}".format(__version__))
  1461. exit(0)
  1462. screen = Gdk.Screen.get_default()
  1463. provider = Gtk.CssProvider()
  1464. style_context = Gtk.StyleContext()
  1465. style_context.add_provider_for_screen(
  1466. screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
  1467. )
  1468. css = b"""
  1469. button#thumb-btn {
  1470. font-weight: normal;
  1471. font-size: 11px;
  1472. }
  1473. button#folder-btn {
  1474. font-size: 12px;
  1475. }
  1476. button#thumb-btn-selected {
  1477. font-weight: bold;
  1478. font-size: 12px;
  1479. border-top: 1px solid #ccc;
  1480. border-left: 1px solid #ccc;
  1481. border-bottom: 1px solid #333;
  1482. border-right: 1px solid #333;
  1483. }
  1484. button#color-btn {
  1485. font-weight: normal;
  1486. }
  1487. button#color-btn-selected {
  1488. font-weight: bold;
  1489. border-top: 1px solid #ccc;
  1490. border-left: 1px solid #ccc;
  1491. border-bottom: 1px solid #333;
  1492. border-right: 1px solid #333;
  1493. }
  1494. button#display-btn {
  1495. font-weight: normal;
  1496. font-size: 12px;
  1497. }
  1498. button#display-btn-selected {
  1499. font-weight: bold;
  1500. font-size: 12px;
  1501. }
  1502. label#image-label {
  1503. font-size: 12px;
  1504. }
  1505. statusbar#status-bar {
  1506. font-size: 12px;
  1507. }
  1508. label#selected-label {
  1509. border-top: 1px solid #ccc;
  1510. border-left: 1px solid #ccc;
  1511. border-bottom: 1px solid #333;
  1512. border-right: 1px solid #333;
  1513. font-size: 12px;
  1514. }
  1515. label#dotfiles {
  1516. font-size: 13px;
  1517. margin-top: 0px;
  1518. margin-bottom: 0px;
  1519. margin-right: 10px;
  1520. }
  1521. label#dotfiles-header {
  1522. font-size: 14px;
  1523. font-weight: bold;
  1524. }
  1525. label#closest {
  1526. font-size: 12px;
  1527. }
  1528. button#dotfiles-button {
  1529. padding: 1px;
  1530. margin: 0px;
  1531. font-size: 12px;
  1532. }
  1533. textview#preview {
  1534. font-size: 13px;
  1535. }
  1536. """
  1537. provider.load_from_data(css)
  1538. set_env(__version__, lang_from_args=lang_from_args) # detect displays, check installed modules, set paths and stuff
  1539. if clear_thumbs:
  1540. clear_thumbnails(clear_all)
  1541. exit()
  1542. common.cols = len(common.displays) if len(common.displays) > common.settings.columns else common.settings.columns
  1543. if common.settings.track_files:
  1544. GLib.timeout_add_seconds(common.settings.tracking_interval_seconds, track_changes)
  1545. if common.env['app_indicator']:
  1546. common.indicator = Indicator()
  1547. # We want Azote to take all the possible screen height. Since Gdk.Screen.height is deprecated, we need to measure
  1548. # the current screen height in another way. `w` is a temporary window.
  1549. # If on sway, we've already detected the screen height in tools/check_displays() and stored it in common.screen_h
  1550. if not common.screen_h: # neither sway, nor Hyprland
  1551. w = TransparentWindow()
  1552. if common.sway or common.env['wm'] == "i3":
  1553. w.fullscreen() # .maximize() doesn't work as expected on sway
  1554. else:
  1555. w.maximize()
  1556. w.present()
  1557. GLib.timeout_add(common.settings.screen_measurement_delay, check_height_and_start, w)
  1558. else:
  1559. if os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
  1560. app = GUI(common.screen_h) # Hyprland
  1561. else:
  1562. app = GUI(int(common.screen_h * 0.95)) # sway
  1563. Gtk.main()
  1564. if __name__ == "__main__":
  1565. sys.exit(main())