mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 21:18:37 +01:00
Adds midi2piano 2016 version to tools
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
This is a remake of 2013 midi2piano tool. Previous version should be considered obsolete.
|
||||
|
||||
Requirements:
|
||||
Python 3
|
||||
|
||||
Simply run midi2piano.py and choose midi file you want to convert.
|
||||
The "sheet music" will be copied to the clipboard.
|
||||
|
||||
There are some constants defined at the top of midi2piano.py.
|
||||
Change their value if needed.
|
||||
|
||||
LINE_LENGTH_LIM - max length of line allowed in the sheet music
|
||||
LINES_LIMIT - max amount of lines allowed in the sheet music. Extra lines will be cropped.
|
||||
|
||||
OVERALL_IMPORT_LIM - max amount of characters allowed in the sheet music.
|
||||
|
||||
|
||||
You can also transpose music if you need to
|
||||
OCTAVE_TRANSPOSE - amount of octaves you melody will be shifted by
|
||||
FLOAT_PRECISION - read comment
|
||||
|
||||
Additional notes:
|
||||
1. Unlike previous midi2piano, this tool optimizes sheet music to fit more in less lines.
|
||||
2. If two notes are less than 50 ms apart, they are chorded. BYOND works in 1/10th of a second so 50 ms is time quanta.
|
||||
4. MIDI event set_tempo is NOT supported. If your MIDI file uses set_tempo to change BPM significantly, consider using some other midi file.
|
||||
|
||||
This tool is considered final.
|
||||
|
||||
|
||||
Made by EditorRUS/Delta Epsilon from Animus Station, ss13.ru
|
||||
Contact me in Discord if you find any major issues: DeltaEpsilon#7787
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
from midi.midi import *
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
This module allows user to convert MIDI melodies to SS13 sheet music ready
|
||||
for copy-and-paste
|
||||
"""
|
||||
from functools import reduce
|
||||
import midi as mi
|
||||
import easygui as egui
|
||||
import pyperclip as pclip
|
||||
|
||||
LINE_LENGTH_LIM = 50
|
||||
LINES_LIMIT = 200
|
||||
OVERALL_IMPORT_LIM = 12000
|
||||
END_OF_LINE_CHAR = """
|
||||
""" # BYOND can't parse \n and I am forced to define my own NEWLINE char
|
||||
|
||||
OCTAVE_TRANSPOSE = 0 # Change here to transpose melodies by octaves
|
||||
FLOAT_PRECISION = 2 # Change here to allow more or less numbers after dot in floats
|
||||
|
||||
OCTAVE_KEYS = 12
|
||||
HIGHEST_OCTAVE = 8
|
||||
|
||||
"""
|
||||
class Meta():
|
||||
version = 1.0
|
||||
integer = 1
|
||||
anti_integer = -1
|
||||
maximum = 1000
|
||||
epsilon = 0.51
|
||||
delta_epsilon = -0.1
|
||||
integral = []
|
||||
tensor = [[],[],[]]
|
||||
o_complexity = epsilon**2
|
||||
random_variance = 0.01
|
||||
"""
|
||||
|
||||
# UTILITY FUNCTIONS
|
||||
def condition(event):
|
||||
"""
|
||||
This function check if given MIDI event is meaningful
|
||||
"""
|
||||
if event[0] == 'track_name' and event[2] == 'Drums': # Percussion
|
||||
return False
|
||||
if event[0] == 'note': # Only thing that matters
|
||||
return True
|
||||
return False
|
||||
|
||||
def notenum2string(num, accidentals, octaves):
|
||||
"""
|
||||
This function converts given notenum to SS13 note according to previous
|
||||
runs expressed using _accidentals_ and _octaves_
|
||||
"""
|
||||
names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||
convert_table = {1:0, 3:1, 6:2, 8:3, 10:4}
|
||||
inclusion_table = {0:0, 2:1, 5:2, 7:3, 9:4}
|
||||
|
||||
num += OCTAVE_KEYS * OCTAVE_TRANSPOSE
|
||||
octave = int(num / OCTAVE_KEYS)
|
||||
if octave < 1 or octave > HIGHEST_OCTAVE:
|
||||
return ["", accidentals, octaves]
|
||||
|
||||
accidentals = accidentals.copy()
|
||||
octaves = octaves.copy()
|
||||
|
||||
output_octaves = list(octaves)
|
||||
name_indx = num % OCTAVE_KEYS
|
||||
|
||||
accidental = (len(names[name_indx]) == 2)
|
||||
output_octaves[name_indx] = octave
|
||||
add_n = False
|
||||
|
||||
if accidental:
|
||||
accidentals[convert_table[name_indx]] = True
|
||||
else:
|
||||
if name_indx in inclusion_table:
|
||||
add_n = accidentals[inclusion_table[name_indx]]
|
||||
accidentals[inclusion_table[name_indx]] = False
|
||||
|
||||
return [
|
||||
(
|
||||
names[name_indx]+
|
||||
("n" if add_n else "")+
|
||||
str((octave if octave != octaves[name_indx] else ""))
|
||||
),
|
||||
accidentals,
|
||||
output_octaves
|
||||
]
|
||||
|
||||
def dur2mod(dur, bpm_mod=1.0):
|
||||
"""
|
||||
This functions returns float representation of duration ready to be
|
||||
added to the note after /
|
||||
"""
|
||||
mod = bpm_mod / dur
|
||||
mod = round(mod, FLOAT_PRECISION)
|
||||
return str(mod).rstrip('0').rstrip('.')
|
||||
# END OF UTILITY FUNCTIONS
|
||||
|
||||
# CONVERSION FUNCTIONS
|
||||
def obtain_midi_file():
|
||||
"""
|
||||
Asks user to select MIDI and returns this file opened in binary mode for reading
|
||||
"""
|
||||
file = egui.fileopenbox(msg='Choose MIDI file to convert',
|
||||
title='MIDI file selection',
|
||||
filetypes=[['*.mid', 'MID files']])
|
||||
if not file:
|
||||
return None
|
||||
file = open(file, mode='rb').read()
|
||||
return file
|
||||
|
||||
def midi2score_without_ticks(midi_file):
|
||||
"""
|
||||
Transforms aforementioned file into a score, truncates it and returns it
|
||||
"""
|
||||
opus = mi.midi2opus(midi_file)
|
||||
opus = mi.to_millisecs(opus)
|
||||
score = mi.opus2score(opus)
|
||||
return score[1:] # Ticks don't matter anymore, it is always 1000
|
||||
|
||||
def filter_events_from_score(score):
|
||||
"""
|
||||
Filters out irrevelant events and returns new score
|
||||
"""
|
||||
return list(map( # For each score track
|
||||
lambda score_track: list(filter( # Filter irrevelant events
|
||||
condition,
|
||||
score_track
|
||||
)),
|
||||
score
|
||||
))
|
||||
|
||||
def filter_empty_tracks(score):
|
||||
"""
|
||||
Filters out empty tracks and returns new score
|
||||
"""
|
||||
return list(filter(
|
||||
lambda score_track: score_track,
|
||||
score))
|
||||
|
||||
|
||||
def filter_start_time_and_note_num(score):
|
||||
"""
|
||||
Recreates score with only note numbers and start time of each note and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda score_track: list(map(
|
||||
lambda event: [event[1], event[4]],
|
||||
score_track)),
|
||||
score))
|
||||
|
||||
def merge_events(score):
|
||||
"""Merges all tracks together and returns new score"""
|
||||
return list(reduce(
|
||||
lambda lst1, lst2: lst1+lst2,
|
||||
score))
|
||||
|
||||
def sort_score_by_event_times(score):
|
||||
"""Sorts events by start time and returns new score"""
|
||||
return list(map(
|
||||
lambda index: score[index],
|
||||
sorted(
|
||||
list(range(len(score))),
|
||||
key=lambda indx: score[indx][0])
|
||||
))
|
||||
|
||||
def convert_into_delta_times(score):
|
||||
"""
|
||||
Transform start_time into delta_time and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda super_event: (
|
||||
[
|
||||
super_event[1][0]-super_event[0][0],
|
||||
super_event[0][1]
|
||||
]), # [ [1, 2], [3, 4] ] -> [ [2, 2] ]
|
||||
zip(score[:-1], score[1:]) # Shifted association. [1, 2, 3] -> [ (1, 2), (2, 3) ]
|
||||
))+[[1000, score[-1][1]]] # Add 1 second note to the end
|
||||
|
||||
def perform_roundation(score):
|
||||
"""
|
||||
Rounds delta times to the nearest multiple of 100 ms as BYOND can't
|
||||
process duration less than that and returns new score
|
||||
"""
|
||||
return list(map(
|
||||
lambda event: [100*round(event[0]/100), event[1]],
|
||||
score))
|
||||
|
||||
def obtain_common_duration(score):
|
||||
"""
|
||||
Returns the most frequent duration throughout the whole melody
|
||||
"""
|
||||
# Parse durations and filter out 0s
|
||||
durs = list(filter(lambda x: x, list(map(lambda event: event[0], score))))
|
||||
unique_durs = []
|
||||
for dur in durs:
|
||||
if dur not in unique_durs:
|
||||
unique_durs.append(dur)
|
||||
# How many such durations occur throughout the melody?
|
||||
counter = [durs.count(dur) for dur in unique_durs]
|
||||
highest_counter = max(counter) # Highest counter
|
||||
dur_n_count = list(zip(durs, counter))
|
||||
dur_n_count = list(filter(lambda e: e[1] == highest_counter, dur_n_count))
|
||||
return dur_n_count[0][0] # Will be there
|
||||
|
||||
def reduce_score_to_chords(score):
|
||||
"""
|
||||
Reforms score into a chord-duration list:
|
||||
[[chord_notes], duration_of_chord]
|
||||
and returns it
|
||||
"""
|
||||
new_score = []
|
||||
new_chord = [[], 0]
|
||||
# [ [chord notes], duration of chord ]
|
||||
for event in score:
|
||||
new_chord[0].append(event[1]) # Append new note to the chord
|
||||
if event[0] == 0:
|
||||
continue # Add new notes to the chord until non-zero duration is hit
|
||||
new_chord[1] = event[0] # This is the duration of chord
|
||||
new_score.append(new_chord) # Append chord to the list
|
||||
new_chord = [[], 0] # Reset the chord
|
||||
return new_score
|
||||
|
||||
def obtain_sheet_music(score, most_frequent_dur):
|
||||
"""
|
||||
Returns unformated sheet music from score
|
||||
"""
|
||||
result = ""
|
||||
|
||||
octaves = [3 for i in range(12)]
|
||||
accidentals = [False for i in range(7)]
|
||||
for event in score:
|
||||
for note_indx in range(len(event[0])):
|
||||
data = notenum2string(event[0][note_indx], accidentals, octaves)
|
||||
result += data[0]
|
||||
accidentals = data[1]
|
||||
octaves = data[2]
|
||||
if note_indx != len(event[0])-1:
|
||||
result += '-'
|
||||
|
||||
if event[1] != most_frequent_dur: # Quarters are default
|
||||
result += '/'
|
||||
result += dur2mod(event[1], most_frequent_dur)
|
||||
result += ','
|
||||
|
||||
return result
|
||||
|
||||
def explode_sheet_music(sheet_music):
|
||||
"""
|
||||
Splits unformatted sheet music into formated lines of LINE_LEN_LIM
|
||||
and such and returns a list of such lines
|
||||
"""
|
||||
split_music = sheet_music.split(',')
|
||||
split_music = list(map(lambda note: note+',', split_music))
|
||||
split_list = []
|
||||
counter = 0
|
||||
line_counter = 1
|
||||
for note in split_music:
|
||||
if line_counter > LINES_LIMIT-1:
|
||||
break
|
||||
if counter+len(note) > LINE_LENGTH_LIM-2:
|
||||
last_note_num = len(split_list)-1
|
||||
split_list[last_note_num] = split_list[last_note_num].rstrip(',')
|
||||
split_list[last_note_num] += END_OF_LINE_CHAR
|
||||
counter = 0
|
||||
line_counter += 1
|
||||
split_list.append(note)
|
||||
counter += len(note)
|
||||
|
||||
return split_list
|
||||
|
||||
def finalize_sheet_music(split_music, most_frequent_dur):
|
||||
"""
|
||||
Recreates sheet music from exploded sheet music, truncates it and returns it
|
||||
"""
|
||||
sheet_music = ""
|
||||
for note in split_music:
|
||||
sheet_music += note
|
||||
sheet_music = sheet_music.rstrip(',') # Trim the last ,
|
||||
sheet_music = "BPM: " + str(int(60000 / most_frequent_dur)) + END_OF_LINE_CHAR + sheet_music
|
||||
return sheet_music[:min(len(sheet_music), OVERALL_IMPORT_LIM)]
|
||||
# END OF CONVERSION FUNCTIONS
|
||||
|
||||
def main_cycle():
|
||||
"""
|
||||
Activate the script
|
||||
"""
|
||||
while True:
|
||||
midi_file = obtain_midi_file()
|
||||
if not midi_file:
|
||||
return # Cancel
|
||||
score = midi2score_without_ticks(midi_file)
|
||||
score = filter_events_from_score(score)
|
||||
score = filter_start_time_and_note_num(score)
|
||||
score = filter_empty_tracks(score)
|
||||
score = merge_events(score)
|
||||
score = sort_score_by_event_times(score)
|
||||
score = convert_into_delta_times(score)
|
||||
score = perform_roundation(score)
|
||||
most_frequent_dur = obtain_common_duration(score)
|
||||
score = reduce_score_to_chords(score)
|
||||
sheet_music = obtain_sheet_music(score, most_frequent_dur)
|
||||
split_music = explode_sheet_music(sheet_music)
|
||||
sheet_music = finalize_sheet_music(split_music, most_frequent_dur)
|
||||
|
||||
pclip.copy(sheet_music)
|
||||
|
||||
main_cycle()
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Pyperclip
|
||||
|
||||
A cross-platform clipboard module for Python. (only handles plain text for now)
|
||||
By Al Sweigart al@inventwithpython.com
|
||||
BSD License
|
||||
|
||||
Usage:
|
||||
import pyperclip
|
||||
pyperclip.copy('The text to be copied to the clipboard.')
|
||||
spam = pyperclip.paste()
|
||||
|
||||
if not pyperclip.copy:
|
||||
print("Copy functionality unavailable!")
|
||||
|
||||
On Windows, no additional modules are needed.
|
||||
On Mac, the module uses pbcopy and pbpaste, which should come with the os.
|
||||
On Linux, install xclip or xsel via package manager. For example, in Debian:
|
||||
sudo apt-get install xclip
|
||||
|
||||
Otherwise on Linux, you will need the gtk or PyQt4 modules installed.
|
||||
|
||||
gtk and PyQt4 modules are not available for Python 3,
|
||||
and this module does not work with PyGObject yet.
|
||||
"""
|
||||
__version__ = '1.5.27'
|
||||
|
||||
import platform
|
||||
import os
|
||||
import subprocess
|
||||
from .clipboards import (init_osx_clipboard,
|
||||
init_gtk_clipboard, init_qt_clipboard,
|
||||
init_xclip_clipboard, init_xsel_clipboard,
|
||||
init_klipper_clipboard, init_no_clipboard)
|
||||
from .windows import init_windows_clipboard
|
||||
|
||||
# `import PyQt4` sys.exit()s if DISPLAY is not in the environment.
|
||||
# Thus, we need to detect the presence of $DISPLAY manually
|
||||
# and not load PyQt4 if it is absent.
|
||||
HAS_DISPLAY = os.getenv("DISPLAY", False)
|
||||
CHECK_CMD = "where" if platform.system() == "Windows" else "which"
|
||||
|
||||
|
||||
def _executable_exists(name):
|
||||
return subprocess.call([CHECK_CMD, name],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
|
||||
|
||||
|
||||
def determine_clipboard():
|
||||
# Determine the OS/platform and set
|
||||
# the copy() and paste() functions accordingly.
|
||||
if 'cygwin' in platform.system().lower():
|
||||
# FIXME: pyperclip currently does not support Cygwin,
|
||||
# see https://github.com/asweigart/pyperclip/issues/55
|
||||
pass
|
||||
elif os.name == 'nt' or platform.system() == 'Windows':
|
||||
return init_windows_clipboard()
|
||||
if os.name == 'mac' or platform.system() == 'Darwin':
|
||||
return init_osx_clipboard()
|
||||
if HAS_DISPLAY:
|
||||
# Determine which command/module is installed, if any.
|
||||
try:
|
||||
import gtk # check if gtk is installed
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return init_gtk_clipboard()
|
||||
|
||||
try:
|
||||
import PyQt4 # check if PyQt4 is installed
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return init_qt_clipboard()
|
||||
|
||||
if _executable_exists("xclip"):
|
||||
return init_xclip_clipboard()
|
||||
if _executable_exists("xsel"):
|
||||
return init_xsel_clipboard()
|
||||
if _executable_exists("klipper") and _executable_exists("qdbus"):
|
||||
return init_klipper_clipboard()
|
||||
|
||||
return init_no_clipboard()
|
||||
|
||||
|
||||
def set_clipboard(clipboard):
|
||||
global copy, paste
|
||||
|
||||
clipboard_types = {'osx': init_osx_clipboard,
|
||||
'gtk': init_gtk_clipboard,
|
||||
'qt': init_qt_clipboard,
|
||||
'xclip': init_xclip_clipboard,
|
||||
'xsel': init_xsel_clipboard,
|
||||
'klipper': init_klipper_clipboard,
|
||||
'windows': init_windows_clipboard,
|
||||
'no': init_no_clipboard}
|
||||
|
||||
copy, paste = clipboard_types[clipboard]()
|
||||
|
||||
|
||||
copy, paste = determine_clipboard()
|
||||
|
||||
__all__ = ["copy", "paste"]
|
||||
@@ -0,0 +1,134 @@
|
||||
import sys
|
||||
import subprocess
|
||||
from .exceptions import PyperclipException
|
||||
|
||||
EXCEPT_MSG = """
|
||||
Pyperclip could not find a copy/paste mechanism for your system.
|
||||
For more information, please visit https://pyperclip.readthedocs.org """
|
||||
PY2 = sys.version_info[0] == 2
|
||||
text_type = unicode if PY2 else str
|
||||
|
||||
|
||||
def init_osx_clipboard():
|
||||
def copy_osx(text):
|
||||
p = subprocess.Popen(['pbcopy', 'w'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_osx():
|
||||
p = subprocess.Popen(['pbpaste', 'r'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_osx, paste_osx
|
||||
|
||||
|
||||
def init_gtk_clipboard():
|
||||
import gtk
|
||||
|
||||
def copy_gtk(text):
|
||||
global cb
|
||||
cb = gtk.Clipboard()
|
||||
cb.set_text(text)
|
||||
cb.store()
|
||||
|
||||
def paste_gtk():
|
||||
clipboardContents = gtk.Clipboard().wait_for_text()
|
||||
# for python 2, returns None if the clipboard is blank.
|
||||
if clipboardContents is None:
|
||||
return ''
|
||||
else:
|
||||
return clipboardContents
|
||||
|
||||
return copy_gtk, paste_gtk
|
||||
|
||||
|
||||
def init_qt_clipboard():
|
||||
# $DISPLAY should exist
|
||||
from PyQt4.QtGui import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
def copy_qt(text):
|
||||
cb = app.clipboard()
|
||||
cb.setText(text)
|
||||
|
||||
def paste_qt():
|
||||
cb = app.clipboard()
|
||||
return text_type(cb.text())
|
||||
|
||||
return copy_qt, paste_qt
|
||||
|
||||
|
||||
def init_xclip_clipboard():
|
||||
def copy_xclip(text):
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xclip():
|
||||
p = subprocess.Popen(['xclip', '-selection', 'c', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xclip, paste_xclip
|
||||
|
||||
|
||||
def init_xsel_clipboard():
|
||||
def copy_xsel(text):
|
||||
p = subprocess.Popen(['xsel', '-b', '-i'],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=text.encode('utf-8'))
|
||||
|
||||
def paste_xsel():
|
||||
p = subprocess.Popen(['xsel', '-b', '-o'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
return stdout.decode('utf-8')
|
||||
|
||||
return copy_xsel, paste_xsel
|
||||
|
||||
|
||||
def init_klipper_clipboard():
|
||||
def copy_klipper(text):
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents',
|
||||
text.encode('utf-8')],
|
||||
stdin=subprocess.PIPE, close_fds=True)
|
||||
p.communicate(input=None)
|
||||
|
||||
def paste_klipper():
|
||||
p = subprocess.Popen(
|
||||
['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'],
|
||||
stdout=subprocess.PIPE, close_fds=True)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
# Workaround for https://bugs.kde.org/show_bug.cgi?id=342874
|
||||
# TODO: https://github.com/asweigart/pyperclip/issues/43
|
||||
clipboardContents = stdout.decode('utf-8')
|
||||
# even if blank, Klipper will append a newline at the end
|
||||
assert len(clipboardContents) > 0
|
||||
# make sure that newline is there
|
||||
assert clipboardContents.endswith('\n')
|
||||
if clipboardContents.endswith('\n'):
|
||||
clipboardContents = clipboardContents[:-1]
|
||||
return clipboardContents
|
||||
|
||||
return copy_klipper, paste_klipper
|
||||
|
||||
|
||||
def init_no_clipboard():
|
||||
class ClipboardUnavailable(object):
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise PyperclipException(EXCEPT_MSG)
|
||||
|
||||
if PY2:
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
else:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
return ClipboardUnavailable(), ClipboardUnavailable()
|
||||
@@ -0,0 +1,11 @@
|
||||
import ctypes
|
||||
|
||||
|
||||
class PyperclipException(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyperclipWindowsException(PyperclipException):
|
||||
def __init__(self, message):
|
||||
message += " (%s)" % ctypes.WinError()
|
||||
super(PyperclipWindowsException, self).__init__(message)
|
||||
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
This module implements clipboard handling on Windows using ctypes.
|
||||
"""
|
||||
import time
|
||||
import contextlib
|
||||
import ctypes
|
||||
from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar
|
||||
from .exceptions import PyperclipWindowsException
|
||||
|
||||
|
||||
class CheckedCall(object):
|
||||
def __init__(self, f):
|
||||
super(CheckedCall, self).__setattr__("f", f)
|
||||
|
||||
def __call__(self, *args):
|
||||
ret = self.f(*args)
|
||||
if not ret and get_errno():
|
||||
raise PyperclipWindowsException("Error calling " + self.f.__name__)
|
||||
return ret
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
setattr(self.f, key, value)
|
||||
|
||||
|
||||
def init_windows_clipboard():
|
||||
from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND,
|
||||
HINSTANCE, HMENU, BOOL, UINT, HANDLE)
|
||||
|
||||
windll = ctypes.windll
|
||||
|
||||
safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA)
|
||||
safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT,
|
||||
INT, INT, HWND, HMENU, HINSTANCE, LPVOID]
|
||||
safeCreateWindowExA.restype = HWND
|
||||
|
||||
safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow)
|
||||
safeDestroyWindow.argtypes = [HWND]
|
||||
safeDestroyWindow.restype = BOOL
|
||||
|
||||
OpenClipboard = windll.user32.OpenClipboard
|
||||
OpenClipboard.argtypes = [HWND]
|
||||
OpenClipboard.restype = BOOL
|
||||
|
||||
safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard)
|
||||
safeCloseClipboard.argtypes = []
|
||||
safeCloseClipboard.restype = BOOL
|
||||
|
||||
safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard)
|
||||
safeEmptyClipboard.argtypes = []
|
||||
safeEmptyClipboard.restype = BOOL
|
||||
|
||||
safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData)
|
||||
safeGetClipboardData.argtypes = [UINT]
|
||||
safeGetClipboardData.restype = HANDLE
|
||||
|
||||
safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData)
|
||||
safeSetClipboardData.argtypes = [UINT, HANDLE]
|
||||
safeSetClipboardData.restype = HANDLE
|
||||
|
||||
safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc)
|
||||
safeGlobalAlloc.argtypes = [UINT, c_size_t]
|
||||
safeGlobalAlloc.restype = HGLOBAL
|
||||
|
||||
safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock)
|
||||
safeGlobalLock.argtypes = [HGLOBAL]
|
||||
safeGlobalLock.restype = LPVOID
|
||||
|
||||
safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock)
|
||||
safeGlobalUnlock.argtypes = [HGLOBAL]
|
||||
safeGlobalUnlock.restype = BOOL
|
||||
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
CF_UNICODETEXT = 13
|
||||
|
||||
@contextlib.contextmanager
|
||||
def window():
|
||||
"""
|
||||
Context that provides a valid Windows hwnd.
|
||||
"""
|
||||
# we really just need the hwnd, so setting "STATIC"
|
||||
# as predefined lpClass is just fine.
|
||||
hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0,
|
||||
None, None, None, None)
|
||||
try:
|
||||
yield hwnd
|
||||
finally:
|
||||
safeDestroyWindow(hwnd)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clipboard(hwnd):
|
||||
"""
|
||||
Context manager that opens the clipboard and prevents
|
||||
other applications from modifying the clipboard content.
|
||||
"""
|
||||
# We may not get the clipboard handle immediately because
|
||||
# some other application is accessing it (?)
|
||||
# We try for at least 500ms to get the clipboard.
|
||||
t = time.time() + 0.5
|
||||
success = False
|
||||
while time.time() < t:
|
||||
success = OpenClipboard(hwnd)
|
||||
if success:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
if not success:
|
||||
raise PyperclipWindowsException("Error calling OpenClipboard")
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
safeCloseClipboard()
|
||||
|
||||
def copy_windows(text):
|
||||
# This function is heavily based on
|
||||
# http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard
|
||||
with window() as hwnd:
|
||||
# http://msdn.com/ms649048
|
||||
# If an application calls OpenClipboard with hwnd set to NULL,
|
||||
# EmptyClipboard sets the clipboard owner to NULL;
|
||||
# this causes SetClipboardData to fail.
|
||||
# => We need a valid hwnd to copy something.
|
||||
with clipboard(hwnd):
|
||||
safeEmptyClipboard()
|
||||
|
||||
if text:
|
||||
# http://msdn.com/ms649051
|
||||
# If the hMem parameter identifies a memory object,
|
||||
# the object must have been allocated using the
|
||||
# function with the GMEM_MOVEABLE flag.
|
||||
count = len(text) + 1
|
||||
handle = safeGlobalAlloc(GMEM_MOVEABLE,
|
||||
count * sizeof(c_wchar))
|
||||
locked_handle = safeGlobalLock(handle)
|
||||
|
||||
ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar))
|
||||
|
||||
safeGlobalUnlock(handle)
|
||||
safeSetClipboardData(CF_UNICODETEXT, handle)
|
||||
|
||||
def paste_windows():
|
||||
with clipboard(None):
|
||||
handle = safeGetClipboardData(CF_UNICODETEXT)
|
||||
if not handle:
|
||||
# GetClipboardData may return NULL with errno == NO_ERROR
|
||||
# if the clipboard is empty.
|
||||
# (Also, it may return a handle to an empty buffer,
|
||||
# but technically that's not empty)
|
||||
return ""
|
||||
return c_wchar_p(handle).value
|
||||
|
||||
return copy_windows, paste_windows
|
||||
Reference in New Issue
Block a user